mirror of
https://github.com/denoland/deno.git
synced 2025-09-24 03:12:29 +00:00
6326 lines
186 KiB
Rust
6326 lines
186 KiB
Rust
// Copyright 2018-2025 the Deno authors. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashSet;
|
|
use std::collections::VecDeque;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::OnceLock;
|
|
|
|
use boxed_error::Boxed;
|
|
use deno_error::JsError;
|
|
use deno_maybe_sync::new_rc;
|
|
use deno_package_json::PackageJson;
|
|
use deno_package_json::PackageJsonDepValue;
|
|
use deno_package_json::PackageJsonDepWorkspaceReq;
|
|
use deno_package_json::PackageJsonLoadError;
|
|
use deno_package_json::PackageJsonRc;
|
|
use deno_path_util::url_from_directory_path;
|
|
use deno_path_util::url_parent;
|
|
use deno_path_util::url_to_file_path;
|
|
use deno_semver::RangeSetOrTag;
|
|
use deno_semver::Version;
|
|
use deno_semver::VersionReq;
|
|
use deno_semver::jsr::JsrDepPackageReq;
|
|
use deno_semver::package::PackageKind;
|
|
use deno_semver::package::PackageNv;
|
|
use deno_semver::package::PackageReq;
|
|
use discovery::ConfigFileDiscovery;
|
|
use discovery::ConfigFolder;
|
|
use discovery::DenoOrPkgJson;
|
|
use discovery::discover_workspace_config_files;
|
|
use indexmap::IndexMap;
|
|
use indexmap::IndexSet;
|
|
use sys_traits::FsMetadata;
|
|
use sys_traits::FsRead;
|
|
use sys_traits::FsReadDir;
|
|
use thiserror::Error;
|
|
use url::Url;
|
|
|
|
use crate::UrlToFilePathError;
|
|
use crate::deno_json;
|
|
use crate::deno_json::BenchConfig;
|
|
use crate::deno_json::CompileConfig;
|
|
use crate::deno_json::CompilerOptions;
|
|
use crate::deno_json::ConfigFile;
|
|
use crate::deno_json::ConfigFileError;
|
|
use crate::deno_json::ConfigFileRc;
|
|
use crate::deno_json::ConfigFileReadError;
|
|
use crate::deno_json::DeployConfig;
|
|
use crate::deno_json::FmtConfig;
|
|
use crate::deno_json::FmtOptionsConfig;
|
|
use crate::deno_json::LinkConfigParseError;
|
|
use crate::deno_json::LintRulesConfig;
|
|
use crate::deno_json::NodeModulesDirMode;
|
|
use crate::deno_json::NodeModulesDirParseError;
|
|
use crate::deno_json::PermissionsConfig;
|
|
use crate::deno_json::PermissionsObjectWithBase;
|
|
use crate::deno_json::PublishConfig;
|
|
pub use crate::deno_json::TaskDefinition;
|
|
use crate::deno_json::TestConfig;
|
|
use crate::deno_json::ToInvalidConfigError;
|
|
use crate::deno_json::ToLockConfigError;
|
|
use crate::deno_json::WorkspaceConfigParseError;
|
|
use crate::glob::FilePatterns;
|
|
use crate::glob::PathOrPattern;
|
|
use crate::glob::PathOrPatternParseError;
|
|
use crate::glob::PathOrPatternSet;
|
|
|
|
mod discovery;
|
|
|
|
#[allow(clippy::disallowed_types)]
|
|
type UrlRc = deno_maybe_sync::MaybeArc<Url>;
|
|
#[allow(clippy::disallowed_types)]
|
|
pub type WorkspaceRc = deno_maybe_sync::MaybeArc<Workspace>;
|
|
#[allow(clippy::disallowed_types)]
|
|
pub type WorkspaceDirectoryRc = deno_maybe_sync::MaybeArc<WorkspaceDirectory>;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ResolverWorkspaceJsrPackage {
|
|
pub base: Url,
|
|
pub name: String,
|
|
pub version: Option<Version>,
|
|
pub exports: IndexMap<String, String>,
|
|
pub is_link: bool,
|
|
}
|
|
|
|
impl ResolverWorkspaceJsrPackage {
|
|
pub fn matches_req(&self, req: &PackageReq) -> bool {
|
|
self.name == req.name
|
|
&& self
|
|
.version
|
|
.as_ref()
|
|
.map(|v| req.version_req.matches(v))
|
|
.unwrap_or(true)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsrPackageConfig {
|
|
/// The package name.
|
|
pub name: String,
|
|
pub member_dir: WorkspaceDirectoryRc,
|
|
pub config_file: ConfigFileRc,
|
|
pub license: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct NpmPackageConfig {
|
|
pub nv: PackageNv,
|
|
pub workspace_dir: WorkspaceDirectoryRc,
|
|
pub pkg_json: PackageJsonRc,
|
|
}
|
|
|
|
impl NpmPackageConfig {
|
|
pub fn matches_req(&self, req: &PackageReq) -> bool {
|
|
self.matches_name_and_version_req(&req.name, &req.version_req)
|
|
}
|
|
|
|
pub fn matches_name_and_version_req(
|
|
&self,
|
|
name: &str,
|
|
version_req: &VersionReq,
|
|
) -> bool {
|
|
if name != self.nv.name {
|
|
return false;
|
|
}
|
|
match version_req.inner() {
|
|
RangeSetOrTag::RangeSet(set) => set.satisfies(&self.nv.version),
|
|
RangeSetOrTag::Tag(tag) => tag == "workspace",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, Hash, PartialEq)]
|
|
pub struct WorkspaceLintConfig {
|
|
pub report: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Error, JsError, PartialEq, Eq)]
|
|
#[class(type)]
|
|
pub enum WorkspaceDiagnosticKind {
|
|
#[error(
|
|
"\"{0}\" field can only be specified in the workspace root deno.json file."
|
|
)]
|
|
RootOnlyOption(&'static str),
|
|
#[error(
|
|
"\"{0}\" field can only be specified in a workspace member deno.json file and not the workspace root file."
|
|
)]
|
|
MemberOnlyOption(&'static str),
|
|
#[error("\"workspaces\" field was ignored. Use \"workspace\" instead.")]
|
|
InvalidWorkspacesOption,
|
|
#[error("\"exports\" field should be specified when specifying a \"name\".")]
|
|
MissingExports,
|
|
#[error(
|
|
"\"importMap\" field is ignored when \"imports\" or \"scopes\" are specified in the config file."
|
|
)]
|
|
ImportMapReferencingImportMap,
|
|
#[error(
|
|
"\"imports\" and \"scopes\" field is ignored when \"importMap\" is specified in the root config file."
|
|
)]
|
|
MemberImportsScopesIgnored,
|
|
#[error(
|
|
"`\"nodeModulesDir\": {previous}` is deprecated in Deno 2.0. Use `\"nodeModulesDir\": \"{suggestion}\"` instead."
|
|
)]
|
|
DeprecatedNodeModulesDirOption {
|
|
previous: bool,
|
|
suggestion: NodeModulesDirMode,
|
|
},
|
|
#[error("\"patch\" property was renamed to \"links\".")]
|
|
DeprecatedPatch,
|
|
#[error(
|
|
"Invalid workspace member name \"{name}\". Ensure the name is in the format '@scope/name'."
|
|
)]
|
|
InvalidMemberName { name: String },
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError, Clone, PartialEq, Eq)]
|
|
#[class(inherit)]
|
|
#[error("{}\n at {}", .kind, .config_url)]
|
|
pub struct WorkspaceDiagnostic {
|
|
#[inherit]
|
|
pub kind: WorkspaceDiagnosticKind,
|
|
pub config_url: Url,
|
|
}
|
|
|
|
#[derive(Debug, JsError, Boxed)]
|
|
pub struct ResolveWorkspaceLinkError(pub Box<ResolveWorkspaceLinkErrorKind>);
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
pub enum ResolveWorkspaceLinkErrorKind {
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
ConfigRead(#[from] ConfigReadError),
|
|
#[class(type)]
|
|
#[error("Could not find link member in '{}'.", .dir_url)]
|
|
NotFound { dir_url: Url },
|
|
#[class(type)]
|
|
#[error("Workspace member cannot be specified as a link.")]
|
|
WorkspaceMemberNotAllowed,
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
InvalidLink(#[from] url::ParseError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
UrlToFilePath(#[from] deno_path_util::UrlToFilePathError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
Workspace(Box<WorkspaceDiscoverError>),
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
pub enum ConfigReadError {
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
DenoJsonRead(#[from] ConfigFileReadError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
PackageJsonRead(#[from] PackageJsonLoadError),
|
|
}
|
|
|
|
#[derive(Debug, JsError, Boxed)]
|
|
#[class(type)]
|
|
pub struct ResolveWorkspaceMemberError(
|
|
pub Box<ResolveWorkspaceMemberErrorKind>,
|
|
);
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
#[class(type)]
|
|
pub enum ResolveWorkspaceMemberErrorKind {
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
ConfigRead(#[from] ConfigReadError),
|
|
#[error("Could not find config file for workspace member in '{}'.", .dir_url)]
|
|
NotFound { dir_url: Url },
|
|
#[error("Could not find package.json for workspace member in '{}'.", .dir_url)]
|
|
NotFoundPackageJson { dir_url: Url },
|
|
#[error("Could not find config file for workspace member in '{}'. Ensure you specify the directory and not the configuration file in the workspace member.", .dir_url)]
|
|
NotFoundMaybeSpecifiedFile { dir_url: Url },
|
|
#[error(
|
|
"Workspace member must be nested in a directory under the workspace.\n Member: {member_url}\n Workspace: {workspace_url}"
|
|
)]
|
|
NonDescendant { workspace_url: Url, member_url: Url },
|
|
#[error("Cannot specify a workspace member twice ('{}').", .member)]
|
|
Duplicate { member: String },
|
|
#[error(
|
|
"The '{name}' package ('{deno_json_url}') cannot have the same name as the package at '{other_deno_json_url}'."
|
|
)]
|
|
DuplicatePackageName {
|
|
name: String,
|
|
deno_json_url: Url,
|
|
other_deno_json_url: Url,
|
|
},
|
|
#[error("Remove the reference to the current config file (\"{}\") in \"workspaces\".", .member)]
|
|
InvalidSelfReference { member: String },
|
|
#[class(inherit)]
|
|
#[error("Invalid workspace member '{}' for config '{}'.", member, base)]
|
|
InvalidMember {
|
|
base: Url,
|
|
member: String,
|
|
#[source]
|
|
#[inherit]
|
|
source: url::ParseError,
|
|
},
|
|
#[class(inherit)]
|
|
#[error(
|
|
"Failed converting {kind} workspace member '{}' to pattern for config '{}'.",
|
|
member,
|
|
base
|
|
)]
|
|
MemberToPattern {
|
|
kind: &'static str,
|
|
base: Url,
|
|
member: String,
|
|
// this error has the text that failed
|
|
#[source]
|
|
#[inherit]
|
|
source: PathOrPatternParseError,
|
|
},
|
|
#[error(transparent)]
|
|
#[class(inherit)]
|
|
UrlToFilePath(#[from] deno_path_util::UrlToFilePathError),
|
|
}
|
|
|
|
#[derive(Debug, JsError, Boxed)]
|
|
#[class(inherit)]
|
|
pub struct WorkspaceDiscoverError(pub Box<WorkspaceDiscoverErrorKind>);
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
#[class(type)]
|
|
pub enum FailedResolvingStartDirectoryError {
|
|
#[error("No paths provided.")]
|
|
NoPathsProvided,
|
|
#[error("Could not resolve path: '{}'.", .0.display())]
|
|
CouldNotResolvePath(PathBuf),
|
|
#[error("Provided config file path ('{}') had no parent directory.", .0.display())]
|
|
PathHasNoParentDirectory(PathBuf),
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
pub enum WorkspaceDiscoverErrorKind {
|
|
#[class(inherit)]
|
|
#[error("Failed resolving start directory.")]
|
|
FailedResolvingStartDirectory(#[source] FailedResolvingStartDirectoryError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
ConfigRead(#[from] ConfigReadError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
PackageJsonRead(#[from] PackageJsonLoadError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
LinkConfigParse(#[from] LinkConfigParseError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
WorkspaceConfigParse(#[from] WorkspaceConfigParseError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
ResolveMember(#[from] ResolveWorkspaceMemberError),
|
|
#[class(inherit)]
|
|
#[error("Failed loading link '{}' in config '{}'.", link, base)]
|
|
ResolveLink {
|
|
link: String,
|
|
base: Url,
|
|
#[source]
|
|
#[inherit]
|
|
source: ResolveWorkspaceLinkError,
|
|
},
|
|
#[class(type)]
|
|
#[error(
|
|
"Command resolved to multiple config files. Ensure all specified paths are within the same workspace.\n First: {base_workspace_url}\n Second: {other_workspace_url}"
|
|
)]
|
|
MultipleWorkspaces {
|
|
base_workspace_url: Url,
|
|
other_workspace_url: Url,
|
|
},
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
UrlToFilePath(#[from] UrlToFilePathError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
PathToUrl(#[from] deno_path_util::PathToUrlError),
|
|
#[class(type)]
|
|
#[error(
|
|
"Config file must be a member of the workspace.\n Config: {config_url}\n Workspace: {workspace_url}"
|
|
)]
|
|
ConfigNotWorkspaceMember { workspace_url: Url, config_url: Url },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum WorkspaceDiscoverStart<'a> {
|
|
Paths(&'a [PathBuf]),
|
|
ConfigFile(&'a Path),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum VendorEnablement<'a> {
|
|
Disable,
|
|
Enable {
|
|
/// The cwd, which will be used when no configuration file is
|
|
/// resolved in order to discover the vendor folder.
|
|
cwd: &'a Path,
|
|
},
|
|
}
|
|
|
|
pub trait WorkspaceCache {
|
|
fn get(&self, dir_path: &Path) -> Option<WorkspaceRc>;
|
|
fn set(&self, dir_path: PathBuf, workspace: WorkspaceRc);
|
|
}
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct WorkspaceDiscoverOptions<'a> {
|
|
/// A cache for deno.json files. This is mostly only useful in the LSP where
|
|
/// workspace discovery may occur multiple times.
|
|
pub deno_json_cache: Option<&'a dyn crate::deno_json::DenoJsonCache>,
|
|
pub pkg_json_cache: Option<&'a dyn deno_package_json::PackageJsonCache>,
|
|
/// A cache for workspaces. This is mostly only useful in the LSP where
|
|
/// workspace discovery may occur multiple times.
|
|
pub workspace_cache: Option<&'a dyn WorkspaceCache>,
|
|
pub additional_config_file_names: &'a [&'a str],
|
|
pub discover_pkg_json: bool,
|
|
pub maybe_vendor_override: Option<VendorEnablement<'a>>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct WorkspaceDirectoryEmptyOptions<'a> {
|
|
pub root_dir: UrlRc,
|
|
pub use_vendor_dir: VendorEnablement<'a>,
|
|
}
|
|
|
|
/// Configuration files found in a specific folder.
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct FolderConfigs {
|
|
pub deno_json: Option<ConfigFileRc>,
|
|
pub pkg_json: Option<PackageJsonRc>,
|
|
}
|
|
|
|
impl FolderConfigs {
|
|
fn from_config_folder(config_folder: ConfigFolder) -> Self {
|
|
match config_folder {
|
|
ConfigFolder::Single(deno_or_pkg_json) => match deno_or_pkg_json {
|
|
DenoOrPkgJson::Deno(deno_json) => FolderConfigs {
|
|
deno_json: Some(deno_json),
|
|
pkg_json: None,
|
|
},
|
|
DenoOrPkgJson::PkgJson(pkg_json) => FolderConfigs {
|
|
deno_json: None,
|
|
pkg_json: Some(pkg_json),
|
|
},
|
|
},
|
|
ConfigFolder::Both {
|
|
deno_json,
|
|
pkg_json,
|
|
} => FolderConfigs {
|
|
deno_json: Some(deno_json),
|
|
pkg_json: Some(pkg_json),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
#[class(type)]
|
|
#[error("lint.report must be a string")]
|
|
pub struct LintConfigError;
|
|
|
|
#[derive(Debug, Default)]
|
|
struct WorkspaceCachedValues {
|
|
dirs: deno_maybe_sync::MaybeDashMap<UrlRc, WorkspaceDirectoryRc>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Workspace {
|
|
root_dir_url: UrlRc,
|
|
config_folders: IndexMap<UrlRc, FolderConfigs>,
|
|
links: BTreeMap<UrlRc, FolderConfigs>,
|
|
pub(crate) vendor_dir: Option<PathBuf>,
|
|
cached: WorkspaceCachedValues,
|
|
}
|
|
|
|
impl Workspace {
|
|
pub(crate) fn new(
|
|
root: ConfigFolder,
|
|
members: BTreeMap<UrlRc, ConfigFolder>,
|
|
link: BTreeMap<UrlRc, ConfigFolder>,
|
|
vendor_dir: Option<PathBuf>,
|
|
) -> Self {
|
|
let root_dir_url = new_rc(root.folder_url());
|
|
let mut config_folders = IndexMap::with_capacity(members.len() + 1);
|
|
config_folders.insert(
|
|
root_dir_url.clone(),
|
|
FolderConfigs::from_config_folder(root),
|
|
);
|
|
config_folders.extend(members.into_iter().map(
|
|
|(folder_url, config_folder)| {
|
|
(folder_url, FolderConfigs::from_config_folder(config_folder))
|
|
},
|
|
));
|
|
Workspace {
|
|
root_dir_url,
|
|
config_folders,
|
|
links: link
|
|
.into_iter()
|
|
.map(|(url, folder)| (url, FolderConfigs::from_config_folder(folder)))
|
|
.collect(),
|
|
vendor_dir,
|
|
cached: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn root_dir_url(&self) -> &UrlRc {
|
|
&self.root_dir_url
|
|
}
|
|
|
|
pub fn root_dir(self: &WorkspaceRc) -> WorkspaceDirectoryRc {
|
|
self.resolve_member_dir(&self.root_dir_url)
|
|
}
|
|
|
|
pub fn root_dir_path(&self) -> PathBuf {
|
|
url_to_file_path(&self.root_dir_url).unwrap()
|
|
}
|
|
|
|
pub fn root_folder_configs(&self) -> &FolderConfigs {
|
|
self.config_folders.get(&self.root_dir_url).unwrap()
|
|
}
|
|
|
|
pub fn root_deno_json(&self) -> Option<&ConfigFileRc> {
|
|
self.root_folder_configs().deno_json.as_ref()
|
|
}
|
|
|
|
pub fn root_pkg_json(&self) -> Option<&PackageJsonRc> {
|
|
self.root_folder_configs().pkg_json.as_ref()
|
|
}
|
|
|
|
pub fn config_folders(&self) -> &IndexMap<UrlRc, FolderConfigs> {
|
|
&self.config_folders
|
|
}
|
|
|
|
/// Gets the folders sorted by whether they have a dependency on each other.
|
|
pub fn config_folders_sorted_by_dependencies(
|
|
&self,
|
|
) -> IndexMap<&UrlRc, &FolderConfigs> {
|
|
struct PackageNameMaybeVersion<'a> {
|
|
name: &'a str,
|
|
version: Option<Version>,
|
|
}
|
|
|
|
enum Dep {
|
|
Req(JsrDepPackageReq),
|
|
Path(Url),
|
|
}
|
|
|
|
impl Dep {
|
|
pub fn matches_pkg(
|
|
&self,
|
|
package_kind: PackageKind,
|
|
pkg: &PackageNameMaybeVersion,
|
|
folder_url: &Url,
|
|
) -> bool {
|
|
match self {
|
|
Dep::Req(req) => {
|
|
req.kind == package_kind
|
|
&& req.req.name == pkg.name
|
|
&& pkg
|
|
.version
|
|
.as_ref()
|
|
.map(|v| {
|
|
// just match if it's a tag
|
|
req.req.version_req.tag().is_some()
|
|
|| req.req.version_req.matches(v)
|
|
})
|
|
.unwrap_or(true)
|
|
}
|
|
Dep::Path(url) => {
|
|
folder_url.as_str().trim_end_matches('/')
|
|
== url.as_str().trim_end_matches('/')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Folder<'a> {
|
|
index: usize,
|
|
dir_url: &'a UrlRc,
|
|
folder: &'a FolderConfigs,
|
|
npm_nv: Option<PackageNameMaybeVersion<'a>>,
|
|
jsr_nv: Option<PackageNameMaybeVersion<'a>>,
|
|
deps: Vec<Dep>,
|
|
}
|
|
|
|
impl<'a> Folder<'a> {
|
|
pub fn depends_on(&self, other: &Folder<'a>) -> bool {
|
|
if let Some(other_nv) = &other.npm_nv
|
|
&& self.has_matching_dep(PackageKind::Npm, other_nv, other.dir_url)
|
|
{
|
|
return true;
|
|
}
|
|
if let Some(other_nv) = &other.jsr_nv
|
|
&& self.has_matching_dep(PackageKind::Jsr, other_nv, other.dir_url)
|
|
{
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn has_matching_dep(
|
|
&self,
|
|
pkg_kind: PackageKind,
|
|
pkg: &PackageNameMaybeVersion,
|
|
folder_url: &Url,
|
|
) -> bool {
|
|
self
|
|
.deps
|
|
.iter()
|
|
.any(|dep| dep.matches_pkg(pkg_kind, pkg, folder_url))
|
|
}
|
|
}
|
|
|
|
let mut folders = Vec::with_capacity(self.config_folders.len());
|
|
for (index, (dir_url, folder)) in self.config_folders.iter().enumerate() {
|
|
folders.push(Folder {
|
|
index,
|
|
folder,
|
|
dir_url,
|
|
jsr_nv: folder.deno_json.as_ref().and_then(|deno_json| {
|
|
deno_json
|
|
.json
|
|
.name
|
|
.as_ref()
|
|
.map(|name| PackageNameMaybeVersion {
|
|
name,
|
|
version: deno_json
|
|
.json
|
|
.version
|
|
.as_ref()
|
|
.and_then(|v| Version::parse_standard(v).ok()),
|
|
})
|
|
}),
|
|
npm_nv: folder.pkg_json.as_ref().and_then(|pkg_json| {
|
|
pkg_json.name.as_ref().map(|name| PackageNameMaybeVersion {
|
|
name,
|
|
version: pkg_json
|
|
.version
|
|
.as_ref()
|
|
.and_then(|v| Version::parse_from_npm(v).ok()),
|
|
})
|
|
}),
|
|
deps: folder
|
|
.deno_json
|
|
.as_ref()
|
|
.map(|d| d.dependencies().into_iter().map(Dep::Req))
|
|
.into_iter()
|
|
.flatten()
|
|
.chain(
|
|
folder
|
|
.pkg_json
|
|
.as_ref()
|
|
.map(|d| {
|
|
let deps = d.resolve_local_package_json_deps();
|
|
deps
|
|
.dependencies
|
|
.iter()
|
|
.chain(deps.dev_dependencies.iter())
|
|
.filter_map(|(k, v)| match v.as_ref().ok()? {
|
|
PackageJsonDepValue::File(path) => {
|
|
dir_url.join(path).ok().map(Dep::Path)
|
|
}
|
|
PackageJsonDepValue::Req(package_req) => {
|
|
Some(Dep::Req(JsrDepPackageReq {
|
|
kind: PackageKind::Npm,
|
|
req: package_req.clone(),
|
|
}))
|
|
}
|
|
PackageJsonDepValue::Workspace(workspace_req) => {
|
|
Some(Dep::Req(JsrDepPackageReq {
|
|
kind: PackageKind::Npm,
|
|
req: PackageReq {
|
|
name: k.clone(),
|
|
version_req: match workspace_req {
|
|
PackageJsonDepWorkspaceReq::VersionReq(
|
|
version_req,
|
|
) => version_req.clone(),
|
|
PackageJsonDepWorkspaceReq::Tilde
|
|
| PackageJsonDepWorkspaceReq::Caret => {
|
|
VersionReq::parse_from_npm("*").unwrap()
|
|
}
|
|
},
|
|
},
|
|
}))
|
|
}
|
|
PackageJsonDepValue::JsrReq(req) => {
|
|
Some(Dep::Req(JsrDepPackageReq {
|
|
kind: PackageKind::Npm,
|
|
req: req.clone(),
|
|
}))
|
|
}
|
|
})
|
|
})
|
|
.into_iter()
|
|
.flatten(),
|
|
)
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
// build adjacency + in-degree
|
|
let n = folders.len();
|
|
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
|
|
let mut indeg = vec![0_u32; n];
|
|
|
|
for i in 0..n {
|
|
for j in 0..n {
|
|
if i != j && folders[i].depends_on(&folders[j]) {
|
|
adj[j].push(i);
|
|
indeg[i] += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// kahn's algorithm
|
|
let mut queue: VecDeque<usize> = indeg
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|&(_, &d)| d == 0)
|
|
.map(|(i, _)| i)
|
|
.collect();
|
|
// preserve original insertion order for deterministic output
|
|
queue.make_contiguous().sort_by_key(|&i| folders[i].index);
|
|
|
|
let mut output = Vec::<usize>::with_capacity(n);
|
|
while let Some(i) = queue.pop_front() {
|
|
output.push(i);
|
|
for &j in &adj[i] {
|
|
indeg[j] -= 1;
|
|
if indeg[j] == 0 {
|
|
queue.push_back(j);
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle possible cycles
|
|
if output.len() < n {
|
|
// collect the still-cyclic nodes
|
|
let mut cyclic: Vec<usize> = (0..n).filter(|&i| indeg[i] > 0).collect();
|
|
|
|
// stable, deterministic: lowest original index first
|
|
cyclic.sort_by_key(|&i| folders[i].index);
|
|
|
|
output.extend(cyclic);
|
|
}
|
|
|
|
output
|
|
.into_iter()
|
|
.map(|i| (folders[i].dir_url, folders[i].folder))
|
|
.collect()
|
|
}
|
|
|
|
pub fn deno_jsons(&self) -> impl Iterator<Item = &ConfigFileRc> {
|
|
self
|
|
.config_folders
|
|
.values()
|
|
.filter_map(|f| f.deno_json.as_ref())
|
|
}
|
|
|
|
pub fn package_jsons(&self) -> impl Iterator<Item = &PackageJsonRc> {
|
|
self
|
|
.config_folders
|
|
.values()
|
|
.filter_map(|f| f.pkg_json.as_ref())
|
|
}
|
|
|
|
#[allow(clippy::needless_lifetimes)] // clippy issue
|
|
pub fn jsr_packages<'a>(
|
|
self: &'a WorkspaceRc,
|
|
) -> impl Iterator<Item = JsrPackageConfig> + 'a {
|
|
self.deno_jsons().filter_map(|c| {
|
|
if !c.is_package() {
|
|
return None;
|
|
}
|
|
Some(JsrPackageConfig {
|
|
member_dir: self.resolve_member_dir(&c.specifier),
|
|
name: c.json.name.clone()?,
|
|
config_file: c.clone(),
|
|
license: c.to_license(),
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn npm_packages(self: &WorkspaceRc) -> Vec<NpmPackageConfig> {
|
|
self
|
|
.package_jsons()
|
|
.filter_map(|c| self.package_json_to_npm_package_config(c))
|
|
.collect()
|
|
}
|
|
|
|
fn package_json_to_npm_package_config(
|
|
self: &WorkspaceRc,
|
|
pkg_json: &PackageJsonRc,
|
|
) -> Option<NpmPackageConfig> {
|
|
Some(NpmPackageConfig {
|
|
workspace_dir: self.resolve_member_dir(&pkg_json.specifier()),
|
|
nv: PackageNv {
|
|
name: deno_semver::StackString::from(pkg_json.name.as_ref()?.as_str()),
|
|
version: {
|
|
let version = pkg_json.version.as_ref()?;
|
|
deno_semver::Version::parse_from_npm(version).ok()?
|
|
},
|
|
},
|
|
pkg_json: pkg_json.clone(),
|
|
})
|
|
}
|
|
|
|
pub fn link_folders(&self) -> &BTreeMap<UrlRc, FolderConfigs> {
|
|
&self.links
|
|
}
|
|
|
|
pub fn link_deno_jsons(&self) -> impl Iterator<Item = &ConfigFileRc> {
|
|
self.links.values().filter_map(|f| f.deno_json.as_ref())
|
|
}
|
|
|
|
pub fn link_pkg_jsons(&self) -> impl Iterator<Item = &PackageJsonRc> {
|
|
self.links.values().filter_map(|f| f.pkg_json.as_ref())
|
|
}
|
|
|
|
pub fn resolver_deno_jsons(&self) -> impl Iterator<Item = &ConfigFileRc> {
|
|
self
|
|
.deno_jsons()
|
|
.chain(self.links.values().filter_map(|f| f.deno_json.as_ref()))
|
|
}
|
|
|
|
pub fn resolver_pkg_jsons(
|
|
&self,
|
|
) -> impl Iterator<Item = (&UrlRc, &PackageJsonRc)> {
|
|
self
|
|
.config_folders
|
|
.iter()
|
|
.filter_map(|(k, v)| Some((k, v.pkg_json.as_ref()?)))
|
|
}
|
|
|
|
pub fn resolver_jsr_pkgs(
|
|
&self,
|
|
) -> impl Iterator<Item = ResolverWorkspaceJsrPackage> + '_ {
|
|
self
|
|
.config_folders
|
|
.iter()
|
|
.filter_map(|(dir_url, f)| Some((dir_url, f.deno_json.as_ref()?, false)))
|
|
.chain(self.links.iter().filter_map(|(dir_url, f)| {
|
|
Some((dir_url, f.deno_json.as_ref()?, true))
|
|
}))
|
|
.filter_map(|(dir_url, config_file, is_link)| {
|
|
let name = config_file.json.name.as_ref()?;
|
|
let version = config_file
|
|
.json
|
|
.version
|
|
.as_ref()
|
|
.and_then(|v| Version::parse_standard(v).ok());
|
|
let exports_config = config_file.to_exports_config().ok()?;
|
|
Some(ResolverWorkspaceJsrPackage {
|
|
is_link,
|
|
base: dir_url.as_ref().clone(),
|
|
name: name.to_string(),
|
|
version,
|
|
exports: exports_config.into_map(),
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn resolve_member_dirs(
|
|
self: &WorkspaceRc,
|
|
) -> impl Iterator<Item = WorkspaceDirectoryRc> {
|
|
self
|
|
.config_folders()
|
|
.keys()
|
|
.map(|url| self.resolve_member_dir(url))
|
|
}
|
|
|
|
/// Resolves a workspace directory, which can be used for deriving
|
|
/// configuration specific to a member.
|
|
pub fn resolve_member_dir(
|
|
self: &WorkspaceRc,
|
|
specifier: &Url,
|
|
) -> WorkspaceDirectoryRc {
|
|
let maybe_folder = self
|
|
.resolve_folder(specifier)
|
|
.filter(|(member_url, _)| **member_url != self.root_dir_url);
|
|
let folder_url = maybe_folder
|
|
.map(|(folder_url, _)| folder_url.clone())
|
|
.unwrap_or_else(|| self.root_dir_url.clone());
|
|
if let Some(dir) = self.cached.dirs.get(&folder_url).map(|d| d.clone()) {
|
|
dir
|
|
} else {
|
|
let workspace_dir = match maybe_folder {
|
|
Some((member_url, folder)) => {
|
|
let maybe_deno_json = folder
|
|
.deno_json
|
|
.as_ref()
|
|
.map(|c| (member_url, c))
|
|
.or_else(|| {
|
|
let parent = parent_specifier_str(member_url.as_str())?;
|
|
self.resolve_deno_json_from_str(parent)
|
|
})
|
|
.or_else(|| {
|
|
let root = self.config_folders.get(&self.root_dir_url).unwrap();
|
|
root.deno_json.as_ref().map(|c| (&self.root_dir_url, c))
|
|
});
|
|
let maybe_pkg_json = folder
|
|
.pkg_json
|
|
.as_ref()
|
|
.map(|pkg_json| (member_url, pkg_json))
|
|
.or_else(|| {
|
|
let parent = parent_specifier_str(member_url.as_str())?;
|
|
self.resolve_pkg_json_from_str(parent)
|
|
})
|
|
.or_else(|| {
|
|
let root = self.config_folders.get(&self.root_dir_url).unwrap();
|
|
root.pkg_json.as_ref().map(|c| (&self.root_dir_url, c))
|
|
});
|
|
WorkspaceDirectory {
|
|
dir_url: member_url.clone(),
|
|
pkg_json: maybe_pkg_json.map(|(member_url, pkg_json)| {
|
|
WorkspaceDirConfig {
|
|
root: if *member_url == self.root_dir_url {
|
|
None
|
|
} else {
|
|
self
|
|
.config_folders
|
|
.get(&self.root_dir_url)
|
|
.unwrap()
|
|
.pkg_json
|
|
.clone()
|
|
},
|
|
member: pkg_json.clone(),
|
|
}
|
|
}),
|
|
deno_json: maybe_deno_json.map(|(member_url, config)| {
|
|
WorkspaceDirConfig {
|
|
root: if self.root_dir_url == *member_url {
|
|
None
|
|
} else {
|
|
self
|
|
.config_folders
|
|
.get(&self.root_dir_url)
|
|
.unwrap()
|
|
.deno_json
|
|
.clone()
|
|
},
|
|
member: config.clone(),
|
|
}
|
|
}),
|
|
workspace: self.clone(),
|
|
cached: Default::default(),
|
|
}
|
|
}
|
|
None => WorkspaceDirectory::create_from_root_folder(self.clone()),
|
|
};
|
|
let workspace_dir = new_rc(workspace_dir);
|
|
self.cached.dirs.insert(folder_url, workspace_dir.clone());
|
|
workspace_dir
|
|
}
|
|
}
|
|
|
|
pub fn resolve_deno_json(
|
|
&self,
|
|
specifier: &Url,
|
|
) -> Option<(&UrlRc, &ConfigFileRc)> {
|
|
self.resolve_deno_json_from_str(specifier.as_str())
|
|
}
|
|
|
|
fn resolve_deno_json_from_str(
|
|
&self,
|
|
specifier: &str,
|
|
) -> Option<(&UrlRc, &ConfigFileRc)> {
|
|
let mut specifier = specifier;
|
|
if !specifier.ends_with('/') {
|
|
specifier = parent_specifier_str(specifier)?;
|
|
}
|
|
loop {
|
|
let (folder_url, folder) = self.resolve_folder_str(specifier)?;
|
|
if let Some(config) = folder.deno_json.as_ref() {
|
|
return Some((folder_url, config));
|
|
}
|
|
specifier = parent_specifier_str(folder_url.as_str())?;
|
|
}
|
|
}
|
|
|
|
fn resolve_pkg_json_from_str(
|
|
&self,
|
|
specifier: &str,
|
|
) -> Option<(&UrlRc, &PackageJsonRc)> {
|
|
let mut specifier = specifier;
|
|
if !specifier.ends_with('/') {
|
|
specifier = parent_specifier_str(specifier)?;
|
|
}
|
|
loop {
|
|
let (folder_url, folder) = self.resolve_folder_str(specifier)?;
|
|
if let Some(pkg_json) = folder.pkg_json.as_ref() {
|
|
return Some((folder_url, pkg_json));
|
|
}
|
|
specifier = parent_specifier_str(folder_url.as_str())?;
|
|
}
|
|
}
|
|
|
|
pub fn resolve_folder(
|
|
&self,
|
|
specifier: &Url,
|
|
) -> Option<(&UrlRc, &FolderConfigs)> {
|
|
self.resolve_folder_str(specifier.as_str())
|
|
}
|
|
|
|
fn resolve_folder_str(
|
|
&self,
|
|
specifier: &str,
|
|
) -> Option<(&UrlRc, &FolderConfigs)> {
|
|
let mut best_match: Option<(&UrlRc, &FolderConfigs)> = None;
|
|
for (dir_url, config) in &self.config_folders {
|
|
if specifier.starts_with(dir_url.as_str())
|
|
&& (best_match.is_none()
|
|
|| dir_url.as_str().len() > best_match.unwrap().0.as_str().len())
|
|
{
|
|
best_match = Some((dir_url, config));
|
|
}
|
|
}
|
|
best_match
|
|
}
|
|
|
|
pub fn diagnostics(&self) -> Vec<WorkspaceDiagnostic> {
|
|
fn check_member_diagnostics(
|
|
member_config: &ConfigFile,
|
|
root_config: Option<&ConfigFile>,
|
|
diagnostics: &mut Vec<WorkspaceDiagnostic>,
|
|
) {
|
|
if member_config.json.import_map.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("importMap"),
|
|
});
|
|
} else if member_config.is_an_import_map()
|
|
&& root_config
|
|
.map(|c| {
|
|
c.json.import_map.is_some()
|
|
&& c.json.imports.is_none()
|
|
&& c.json.scopes.is_none()
|
|
})
|
|
.unwrap_or(false)
|
|
{
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::MemberImportsScopesIgnored,
|
|
});
|
|
}
|
|
if member_config.json.lock.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("lock"),
|
|
});
|
|
}
|
|
if member_config.json.node_modules_dir.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("nodeModulesDir"),
|
|
});
|
|
}
|
|
if member_config.json.links.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("links"),
|
|
});
|
|
}
|
|
if member_config.json.scopes.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("scopes"),
|
|
});
|
|
}
|
|
if !member_config.json.unstable.is_empty() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("unstable"),
|
|
});
|
|
}
|
|
if member_config.json.vendor.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("vendor"),
|
|
});
|
|
}
|
|
if member_config.json.workspace.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("workspace"),
|
|
});
|
|
}
|
|
if let Some(value) = &member_config.json.lint
|
|
&& value.get("report").is_some()
|
|
{
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: member_config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("lint.report"),
|
|
});
|
|
}
|
|
}
|
|
|
|
fn check_all_configs(
|
|
config: &ConfigFile,
|
|
diagnostics: &mut Vec<WorkspaceDiagnostic>,
|
|
) {
|
|
if let Some(name) = &config.json.name
|
|
&& !is_valid_jsr_pkg_name(name)
|
|
{
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::InvalidMemberName {
|
|
name: name.clone(),
|
|
},
|
|
});
|
|
}
|
|
if config.json.deprecated_workspaces.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::InvalidWorkspacesOption,
|
|
});
|
|
}
|
|
if config.json.deprecated_patch.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::DeprecatedPatch,
|
|
});
|
|
}
|
|
if config.json.name.is_some() && config.json.exports.is_none() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::MissingExports,
|
|
});
|
|
}
|
|
if config.is_an_import_map() && config.json.import_map.is_some() {
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::ImportMapReferencingImportMap,
|
|
});
|
|
}
|
|
if let Some(serde_json::Value::Bool(enabled)) =
|
|
&config.json.node_modules_dir
|
|
{
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::DeprecatedNodeModulesDirOption {
|
|
previous: *enabled,
|
|
suggestion: if config.json.unstable.iter().any(|v| v == "byonm") {
|
|
NodeModulesDirMode::Manual
|
|
} else if *enabled {
|
|
NodeModulesDirMode::Auto
|
|
} else {
|
|
NodeModulesDirMode::None
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
let mut diagnostics = Vec::new();
|
|
for (url, folder) in &self.config_folders {
|
|
if let Some(config) = &folder.deno_json {
|
|
let is_root = url == &self.root_dir_url;
|
|
if !is_root {
|
|
check_member_diagnostics(
|
|
config,
|
|
self.root_deno_json().map(|r| r.as_ref()),
|
|
&mut diagnostics,
|
|
);
|
|
}
|
|
|
|
check_all_configs(config, &mut diagnostics);
|
|
}
|
|
}
|
|
|
|
for folder in self.links.values() {
|
|
if let Some(config) = &folder.deno_json
|
|
&& config.json.links.is_some()
|
|
{
|
|
// supporting linking in links is too complicated
|
|
diagnostics.push(WorkspaceDiagnostic {
|
|
config_url: config.specifier.clone(),
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("links"),
|
|
});
|
|
}
|
|
}
|
|
|
|
diagnostics
|
|
}
|
|
|
|
pub fn vendor_dir_path(&self) -> Option<&PathBuf> {
|
|
self.vendor_dir.as_ref()
|
|
}
|
|
|
|
pub fn to_lint_config(&self) -> Result<WorkspaceLintConfig, LintConfigError> {
|
|
self
|
|
.with_root_config_only(|root_config| {
|
|
Ok(WorkspaceLintConfig {
|
|
report: match root_config
|
|
.json
|
|
.lint
|
|
.as_ref()
|
|
.and_then(|l| l.get("report"))
|
|
{
|
|
Some(report) => match report {
|
|
serde_json::Value::String(value) => Some(value.to_string()),
|
|
serde_json::Value::Null => None,
|
|
serde_json::Value::Bool(_)
|
|
| serde_json::Value::Number(_)
|
|
| serde_json::Value::Array(_)
|
|
| serde_json::Value::Object(_) => {
|
|
return Err(LintConfigError);
|
|
}
|
|
},
|
|
None => None,
|
|
},
|
|
})
|
|
})
|
|
.unwrap_or(Ok(Default::default()))
|
|
}
|
|
|
|
pub fn to_import_map_path(&self) -> Result<Option<PathBuf>, ConfigFileError> {
|
|
self
|
|
.with_root_config_only(|root_config| root_config.to_import_map_path())
|
|
.unwrap_or(Ok(None))
|
|
}
|
|
|
|
pub fn resolve_lockfile_path(
|
|
&self,
|
|
) -> Result<Option<PathBuf>, ToLockConfigError> {
|
|
if let Some(deno_json) = self.root_deno_json() {
|
|
Ok(deno_json.resolve_lockfile_path()?)
|
|
} else if let Some(pkg_json) = self.root_pkg_json() {
|
|
Ok(pkg_json.path.parent().map(|p| p.join("deno.lock")))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
pub fn resolve_bench_config_for_members(
|
|
self: &WorkspaceRc,
|
|
cli_args: &FilePatterns,
|
|
) -> Result<Vec<(WorkspaceDirectoryRc, BenchConfig)>, ToInvalidConfigError>
|
|
{
|
|
self.resolve_config_for_members(cli_args, |dir, patterns| {
|
|
dir.to_bench_config(patterns)
|
|
})
|
|
}
|
|
|
|
pub fn resolve_lint_config_for_members(
|
|
self: &WorkspaceRc,
|
|
cli_args: &FilePatterns,
|
|
) -> Result<
|
|
Vec<(WorkspaceDirectoryRc, WorkspaceDirLintConfig)>,
|
|
ToInvalidConfigError,
|
|
> {
|
|
self.resolve_config_for_members(cli_args, |dir, patterns| {
|
|
dir.to_lint_config(patterns)
|
|
})
|
|
}
|
|
|
|
pub fn resolve_fmt_config_for_members(
|
|
self: &WorkspaceRc,
|
|
cli_args: &FilePatterns,
|
|
) -> Result<Vec<(WorkspaceDirectoryRc, FmtConfig)>, ToInvalidConfigError> {
|
|
self.resolve_config_for_members(cli_args, |dir, patterns| {
|
|
dir.to_fmt_config(patterns)
|
|
})
|
|
}
|
|
|
|
pub fn resolve_test_config_for_members(
|
|
self: &WorkspaceRc,
|
|
cli_args: &FilePatterns,
|
|
) -> Result<Vec<(WorkspaceDirectoryRc, TestConfig)>, ToInvalidConfigError> {
|
|
self.resolve_config_for_members(cli_args, |dir, patterns| {
|
|
dir.to_test_config(patterns)
|
|
})
|
|
}
|
|
|
|
fn resolve_config_for_members<TConfig, E>(
|
|
self: &WorkspaceRc,
|
|
cli_args: &FilePatterns,
|
|
resolve_config: impl Fn(&WorkspaceDirectory, FilePatterns) -> Result<TConfig, E>,
|
|
) -> Result<Vec<(WorkspaceDirectoryRc, TConfig)>, E> {
|
|
let cli_args_by_folder = self.split_cli_args_by_deno_json_folder(cli_args);
|
|
let mut result = Vec::with_capacity(cli_args_by_folder.len());
|
|
for (folder_url, patterns) in cli_args_by_folder {
|
|
let dir = self.resolve_member_dir(&folder_url);
|
|
let config = resolve_config(&dir, patterns)?;
|
|
result.push((dir, config));
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
fn split_cli_args_by_deno_json_folder(
|
|
&self,
|
|
cli_args: &FilePatterns,
|
|
) -> IndexMap<UrlRc, FilePatterns> {
|
|
fn common_ancestor(a: &Path, b: &Path) -> PathBuf {
|
|
a.components()
|
|
.zip(b.components())
|
|
.take_while(|(a, b)| a == b)
|
|
.map(|(a, _)| a)
|
|
.collect()
|
|
}
|
|
|
|
let cli_arg_patterns = cli_args.split_by_base();
|
|
let deno_json_folders = self
|
|
.config_folders
|
|
.iter()
|
|
.filter(|(_, folder)| folder.deno_json.is_some())
|
|
.map(|(url, folder)| {
|
|
let dir_path = url_to_file_path(url).unwrap();
|
|
(dir_path, (url, folder))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut results: IndexMap<_, FilePatterns> =
|
|
IndexMap::with_capacity(deno_json_folders.len() + 1);
|
|
for pattern in cli_arg_patterns {
|
|
let mut matches = Vec::with_capacity(deno_json_folders.len());
|
|
for (dir_path, v) in deno_json_folders.iter() {
|
|
if pattern.base.starts_with(dir_path)
|
|
|| dir_path.starts_with(&pattern.base)
|
|
{
|
|
matches.push((dir_path, *v));
|
|
}
|
|
}
|
|
// remove any non-sub/current folders that start with another folder
|
|
let mut indexes_to_remove = VecDeque::with_capacity(matches.len());
|
|
for (i, (m, _)) in matches.iter().enumerate() {
|
|
if !m.starts_with(&pattern.base)
|
|
&& matches.iter().any(|(sub, _)| {
|
|
sub.starts_with(m) && sub != m && pattern.base.starts_with(m)
|
|
})
|
|
{
|
|
indexes_to_remove.push_back(i);
|
|
}
|
|
}
|
|
let mut matched_folder_urls =
|
|
Vec::with_capacity(std::cmp::max(1, matches.len()));
|
|
if matches.is_empty() {
|
|
// This will occur when someone specifies a file that's outside
|
|
// the workspace directory. In this case, use the root directory's config
|
|
// so that it's consistent across the workspace.
|
|
matched_folder_urls.push(&self.root_dir_url);
|
|
}
|
|
for (i, (_dir_path, (folder_url, _config))) in matches.iter().enumerate()
|
|
{
|
|
if let Some(skip_index) = indexes_to_remove.front()
|
|
&& i == *skip_index
|
|
{
|
|
indexes_to_remove.pop_front();
|
|
continue;
|
|
}
|
|
matched_folder_urls.push(folder_url);
|
|
}
|
|
for folder_url in matched_folder_urls {
|
|
let entry = results.entry((*folder_url).clone());
|
|
let folder_path = url_to_file_path(folder_url).unwrap();
|
|
match entry {
|
|
indexmap::map::Entry::Occupied(entry) => {
|
|
let entry = entry.into_mut();
|
|
let common_base = common_ancestor(&pattern.base, &entry.base);
|
|
if common_base.starts_with(&folder_path)
|
|
&& entry.base.starts_with(&common_base)
|
|
{
|
|
entry.base = common_base;
|
|
}
|
|
match &mut entry.include {
|
|
Some(set) => {
|
|
if let Some(includes) = &pattern.include {
|
|
for include in includes.inner() {
|
|
if !set.inner().contains(include) {
|
|
set.push(include.clone())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
entry.include.clone_from(&pattern.include);
|
|
}
|
|
}
|
|
}
|
|
indexmap::map::Entry::Vacant(entry) => {
|
|
entry.insert(FilePatterns {
|
|
base: if pattern.base.starts_with(&folder_path) {
|
|
pattern.base.clone()
|
|
} else {
|
|
folder_path.clone()
|
|
},
|
|
include: pattern.include.clone(),
|
|
exclude: pattern.exclude.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
results
|
|
}
|
|
|
|
pub fn resolve_config_excludes(
|
|
&self,
|
|
) -> Result<PathOrPatternSet, ToInvalidConfigError> {
|
|
// have the root excludes at the front because they're lower priority
|
|
let mut excludes = match &self.root_deno_json() {
|
|
Some(c) => c.to_exclude_files_config()?.exclude.into_path_or_patterns(),
|
|
None => Default::default(),
|
|
};
|
|
for (dir_url, folder) in self.config_folders.iter() {
|
|
let Some(deno_json) = folder.deno_json.as_ref() else {
|
|
continue;
|
|
};
|
|
if dir_url == &self.root_dir_url {
|
|
continue;
|
|
}
|
|
excludes.extend(
|
|
deno_json
|
|
.to_exclude_files_config()?
|
|
.exclude
|
|
.into_path_or_patterns(),
|
|
);
|
|
}
|
|
Ok(PathOrPatternSet::new(excludes))
|
|
}
|
|
|
|
pub fn unstable_features(&self) -> &[String] {
|
|
self
|
|
.with_root_config_only(|deno_json| {
|
|
(&deno_json.json.unstable) as &[String]
|
|
})
|
|
.unwrap_or(&[])
|
|
}
|
|
|
|
pub fn has_unstable(&self, name: &str) -> bool {
|
|
self
|
|
.with_root_config_only(|deno_json| deno_json.has_unstable(name))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn with_root_config_only<'a, R>(
|
|
&'a self,
|
|
with_root: impl Fn(&'a ConfigFile) -> R,
|
|
) -> Option<R> {
|
|
self.root_deno_json().map(|c| with_root(c))
|
|
}
|
|
|
|
pub fn node_modules_dir(
|
|
&self,
|
|
) -> Result<Option<NodeModulesDirMode>, deno_json::NodeModulesDirParseError>
|
|
{
|
|
self
|
|
.root_deno_json()
|
|
.and_then(|c| c.json.node_modules_dir.as_ref())
|
|
.map(|v| {
|
|
serde_json::from_value::<NodeModulesDirMode>(v.clone())
|
|
.map_err(|err| NodeModulesDirParseError { source: err })
|
|
})
|
|
.transpose()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct WorkspaceDirConfig<T> {
|
|
#[allow(clippy::disallowed_types)]
|
|
member: deno_maybe_sync::MaybeArc<T>,
|
|
// will be None when it doesn't exist or the member config
|
|
// is the root config
|
|
#[allow(clippy::disallowed_types)]
|
|
root: Option<deno_maybe_sync::MaybeArc<T>>,
|
|
}
|
|
|
|
#[derive(Debug, Error, JsError)]
|
|
#[class(inherit)]
|
|
#[error("Failed parsing '{specifier}'.")]
|
|
pub struct ToTasksConfigError {
|
|
specifier: Url,
|
|
#[source]
|
|
#[inherit]
|
|
error: ToInvalidConfigError,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Hash, PartialEq)]
|
|
pub struct WorkspaceDirLintConfig {
|
|
pub rules: LintRulesConfig,
|
|
pub plugins: Vec<Url>,
|
|
pub files: FilePatterns,
|
|
}
|
|
|
|
/// Represents the "default" type library that should be used when type
|
|
/// checking the code in the module graph. Note that a user provided config
|
|
/// of `"lib"` would override this value.
|
|
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
|
pub enum TsTypeLib {
|
|
DenoWindow,
|
|
DenoWorker,
|
|
}
|
|
|
|
impl Default for TsTypeLib {
|
|
fn default() -> Self {
|
|
Self::DenoWindow
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CompilerOptionsSource {
|
|
pub specifier: UrlRc,
|
|
pub compiler_options: Option<CompilerOptions>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
struct CachedDirectoryValues {
|
|
permissions: OnceLock<PermissionsConfig>,
|
|
bench: OnceLock<BenchConfig>,
|
|
compile: OnceLock<CompileConfig>,
|
|
test: OnceLock<TestConfig>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct WorkspaceDirectory {
|
|
pub workspace: WorkspaceRc,
|
|
/// The directory that this context is for. This is generally the cwd.
|
|
dir_url: UrlRc,
|
|
pkg_json: Option<WorkspaceDirConfig<PackageJson>>,
|
|
deno_json: Option<WorkspaceDirConfig<ConfigFile>>,
|
|
cached: CachedDirectoryValues,
|
|
}
|
|
|
|
impl WorkspaceDirectory {
|
|
pub fn empty(opts: WorkspaceDirectoryEmptyOptions) -> WorkspaceDirectoryRc {
|
|
let workspace = new_rc(Workspace {
|
|
config_folders: IndexMap::from([(
|
|
opts.root_dir.clone(),
|
|
FolderConfigs::default(),
|
|
)]),
|
|
root_dir_url: opts.root_dir.clone(),
|
|
links: BTreeMap::new(),
|
|
vendor_dir: match opts.use_vendor_dir {
|
|
VendorEnablement::Enable { cwd } => Some(cwd.join("vendor")),
|
|
VendorEnablement::Disable => None,
|
|
},
|
|
cached: Default::default(),
|
|
});
|
|
workspace.resolve_member_dir(&opts.root_dir)
|
|
}
|
|
|
|
pub fn discover<TSys: FsMetadata + FsRead + FsReadDir>(
|
|
sys: &TSys,
|
|
start: WorkspaceDiscoverStart,
|
|
opts: &WorkspaceDiscoverOptions,
|
|
) -> Result<WorkspaceDirectoryRc, WorkspaceDiscoverError> {
|
|
fn resolve_start_dir(
|
|
sys: &impl FsMetadata,
|
|
start: &WorkspaceDiscoverStart,
|
|
) -> Result<Url, WorkspaceDiscoverError> {
|
|
match start {
|
|
WorkspaceDiscoverStart::Paths(paths) => {
|
|
if paths.is_empty() {
|
|
Err(
|
|
WorkspaceDiscoverErrorKind::FailedResolvingStartDirectory(
|
|
FailedResolvingStartDirectoryError::NoPathsProvided,
|
|
)
|
|
.into(),
|
|
)
|
|
} else {
|
|
// just select the first one... this doesn't matter too much
|
|
// at the moment because we only use this for lint and fmt,
|
|
// so this is ok for now
|
|
let path = &paths[0];
|
|
match sys.fs_is_dir(path) {
|
|
Ok(is_dir) => Ok(
|
|
url_from_directory_path(if is_dir {
|
|
path
|
|
} else {
|
|
path.parent().unwrap()
|
|
})
|
|
.unwrap(),
|
|
),
|
|
Err(_err) => {
|
|
// assume the parent is a directory
|
|
match path.parent() {
|
|
Some(parent) => Ok(url_from_directory_path(parent).unwrap()),
|
|
None => Err(
|
|
WorkspaceDiscoverErrorKind::FailedResolvingStartDirectory(
|
|
FailedResolvingStartDirectoryError::CouldNotResolvePath(
|
|
path.clone(),
|
|
),
|
|
)
|
|
.into(),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
WorkspaceDiscoverStart::ConfigFile(path) => {
|
|
let parent = path.parent().ok_or_else(|| {
|
|
WorkspaceDiscoverErrorKind::FailedResolvingStartDirectory(
|
|
FailedResolvingStartDirectoryError::PathHasNoParentDirectory(
|
|
path.to_path_buf(),
|
|
),
|
|
)
|
|
})?;
|
|
Ok(url_from_directory_path(parent).unwrap())
|
|
}
|
|
}
|
|
}
|
|
|
|
let start_dir = resolve_start_dir(sys, &start)?;
|
|
let config_file_discovery =
|
|
discover_workspace_config_files(sys, start, opts)?;
|
|
|
|
let context = match config_file_discovery {
|
|
ConfigFileDiscovery::None {
|
|
maybe_vendor_dir: vendor_dir,
|
|
} => {
|
|
let start_dir = new_rc(start_dir);
|
|
let workspace = new_rc(Workspace {
|
|
config_folders: IndexMap::from([(
|
|
start_dir.clone(),
|
|
FolderConfigs::default(),
|
|
)]),
|
|
root_dir_url: start_dir.clone(),
|
|
links: BTreeMap::new(),
|
|
vendor_dir,
|
|
cached: Default::default(),
|
|
});
|
|
workspace.resolve_member_dir(&start_dir)
|
|
}
|
|
ConfigFileDiscovery::Workspace { workspace } => {
|
|
workspace.resolve_member_dir(&start_dir)
|
|
}
|
|
};
|
|
debug_assert!(
|
|
context
|
|
.workspace
|
|
.config_folders
|
|
.contains_key(&context.workspace.root_dir_url),
|
|
"root should always have a folder"
|
|
);
|
|
Ok(context)
|
|
}
|
|
|
|
fn create_from_root_folder(workspace: WorkspaceRc) -> Self {
|
|
let root_folder = workspace
|
|
.config_folders
|
|
.get(&workspace.root_dir_url)
|
|
.unwrap();
|
|
let dir_url = workspace.root_dir_url.clone();
|
|
WorkspaceDirectory {
|
|
dir_url,
|
|
pkg_json: root_folder.pkg_json.as_ref().map(|config| {
|
|
WorkspaceDirConfig {
|
|
member: config.clone(),
|
|
root: None,
|
|
}
|
|
}),
|
|
deno_json: root_folder.deno_json.as_ref().map(|config| {
|
|
WorkspaceDirConfig {
|
|
member: config.clone(),
|
|
root: None,
|
|
}
|
|
}),
|
|
workspace,
|
|
cached: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn jsr_packages_for_publish(
|
|
self: &WorkspaceDirectoryRc,
|
|
) -> Vec<JsrPackageConfig> {
|
|
// only publish the current folder if it's a package
|
|
if let Some(package_config) = self.maybe_package_config() {
|
|
return vec![package_config];
|
|
}
|
|
if let Some(pkg_json) = &self.pkg_json {
|
|
let dir_path = url_to_file_path(&self.dir_url).unwrap();
|
|
// don't publish anything if in a package.json only directory within
|
|
// a workspace
|
|
if pkg_json.member.dir_path().starts_with(&dir_path)
|
|
&& dir_path != pkg_json.member.dir_path()
|
|
{
|
|
return Vec::new();
|
|
}
|
|
}
|
|
if self.dir_url == self.workspace.root_dir_url {
|
|
self.workspace.jsr_packages().collect()
|
|
} else {
|
|
// nothing to publish
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
pub fn dir_url(&self) -> &UrlRc {
|
|
&self.dir_url
|
|
}
|
|
|
|
pub fn dir_path(&self) -> PathBuf {
|
|
url_to_file_path(&self.dir_url).unwrap()
|
|
}
|
|
|
|
pub fn has_deno_or_pkg_json(&self) -> bool {
|
|
self.has_pkg_json() || self.has_deno_json()
|
|
}
|
|
|
|
pub fn has_deno_json(&self) -> bool {
|
|
self.deno_json.is_some()
|
|
}
|
|
|
|
pub fn has_pkg_json(&self) -> bool {
|
|
self.pkg_json.is_some()
|
|
}
|
|
|
|
pub fn maybe_deno_json(&self) -> Option<&ConfigFileRc> {
|
|
self.deno_json.as_ref().map(|c| &c.member)
|
|
}
|
|
|
|
pub fn maybe_pkg_json(&self) -> Option<&PackageJsonRc> {
|
|
self.pkg_json.as_ref().map(|c| &c.member)
|
|
}
|
|
|
|
pub fn maybe_package_config(
|
|
self: &WorkspaceDirectoryRc,
|
|
) -> Option<JsrPackageConfig> {
|
|
let deno_json = self.maybe_deno_json()?;
|
|
let pkg_name = deno_json.json.name.as_ref()?;
|
|
if !deno_json.is_package() {
|
|
return None;
|
|
}
|
|
Some(JsrPackageConfig {
|
|
name: pkg_name.clone(),
|
|
config_file: deno_json.clone(),
|
|
member_dir: self.clone(),
|
|
license: deno_json.to_license(),
|
|
})
|
|
}
|
|
|
|
/// Gets a list of raw compiler options that the user provided, in a vec of
|
|
/// size 0-2 based on `[maybe_root, maybe_member].flatten()`.
|
|
pub fn to_configured_compiler_options_sources(
|
|
&self,
|
|
) -> Vec<CompilerOptionsSource> {
|
|
let Some(deno_json) = self.deno_json.as_ref() else {
|
|
return Vec::new();
|
|
};
|
|
let root = deno_json.root.as_ref().map(|d| CompilerOptionsSource {
|
|
specifier: new_rc(d.specifier.clone()),
|
|
compiler_options: d
|
|
.json
|
|
.compiler_options
|
|
.as_ref()
|
|
.filter(|v| !v.is_null())
|
|
.cloned()
|
|
.map(CompilerOptions),
|
|
});
|
|
let member = CompilerOptionsSource {
|
|
specifier: new_rc(deno_json.member.specifier.clone()),
|
|
compiler_options: deno_json
|
|
.member
|
|
.json
|
|
.compiler_options
|
|
.as_ref()
|
|
.filter(|v| !v.is_null())
|
|
.cloned()
|
|
.map(CompilerOptions),
|
|
};
|
|
root.into_iter().chain([member]).collect()
|
|
}
|
|
|
|
pub fn to_lint_config(
|
|
&self,
|
|
cli_args: FilePatterns,
|
|
) -> Result<WorkspaceDirLintConfig, ToInvalidConfigError> {
|
|
let mut config = self.to_lint_config_inner()?;
|
|
self.exclude_includes_with_member_for_base_for_root(&mut config.files);
|
|
combine_files_config_with_cli_args(&mut config.files, cli_args);
|
|
self.append_workspace_members_to_exclude(&mut config.files);
|
|
Ok(config)
|
|
}
|
|
|
|
fn to_lint_config_inner(
|
|
&self,
|
|
) -> Result<WorkspaceDirLintConfig, ToInvalidConfigError> {
|
|
let Some(deno_json) = self.deno_json.as_ref() else {
|
|
return Ok(WorkspaceDirLintConfig {
|
|
rules: Default::default(),
|
|
plugins: Default::default(),
|
|
files: FilePatterns::new_with_base(
|
|
url_to_file_path(&self.dir_url).unwrap(),
|
|
),
|
|
});
|
|
};
|
|
let member_config = deno_json.member.to_lint_config()?;
|
|
let root_config = deno_json
|
|
.root
|
|
.as_ref()
|
|
.map(|root| root.to_lint_config())
|
|
.transpose()?;
|
|
|
|
// 1. Merge workspace root + member plugins
|
|
// 2. Workspace member can filter out plugins by negating
|
|
// like this: `!my-plugin`
|
|
// 3. Remove duplicates in case a plugin was defined in both
|
|
// workspace root and member.
|
|
let excluded_plugins = member_config
|
|
.options
|
|
.plugins
|
|
.iter()
|
|
.filter(|plugin| plugin.specifier.starts_with('!'))
|
|
.map(|plugin| {
|
|
deno_json
|
|
.member
|
|
.specifier
|
|
.join(&plugin.specifier[1..])
|
|
.map_err(|err| ToInvalidConfigError::InvalidConfig {
|
|
config: "lint",
|
|
source: err.into(),
|
|
})
|
|
})
|
|
.collect::<Result<HashSet<_>, _>>()?;
|
|
|
|
let plugins = root_config
|
|
.iter()
|
|
.flat_map(|root_config| &root_config.options.plugins)
|
|
.chain(&member_config.options.plugins)
|
|
.filter(|plugin| !plugin.specifier.starts_with('!'))
|
|
.map(|plugin| {
|
|
plugin.base.join(&plugin.specifier).map_err(|err| {
|
|
ToInvalidConfigError::InvalidConfig {
|
|
config: "lint",
|
|
source: err.into(),
|
|
}
|
|
})
|
|
})
|
|
.collect::<Result<IndexSet<_>, _>>()?
|
|
.into_iter()
|
|
.filter(|plugin| !excluded_plugins.contains(plugin))
|
|
.collect::<Vec<_>>();
|
|
|
|
let (rules, files) = match root_config {
|
|
Some(root_config) => (
|
|
LintRulesConfig {
|
|
tags: combine_option_vecs(
|
|
root_config.options.rules.tags,
|
|
member_config.options.rules.tags,
|
|
),
|
|
include: combine_option_vecs_with_override(
|
|
CombineOptionVecsWithOverride {
|
|
root: root_config.options.rules.include,
|
|
member: member_config
|
|
.options
|
|
.rules
|
|
.include
|
|
.as_ref()
|
|
.map(Cow::Borrowed),
|
|
member_override_root: member_config
|
|
.options
|
|
.rules
|
|
.exclude
|
|
.as_ref(),
|
|
},
|
|
),
|
|
exclude: combine_option_vecs_with_override(
|
|
CombineOptionVecsWithOverride {
|
|
root: root_config.options.rules.exclude,
|
|
member: member_config.options.rules.exclude.map(Cow::Owned),
|
|
member_override_root: member_config
|
|
.options
|
|
.rules
|
|
.include
|
|
.as_ref(),
|
|
},
|
|
),
|
|
},
|
|
combine_patterns(root_config.files, member_config.files),
|
|
),
|
|
None => (member_config.options.rules, member_config.files),
|
|
};
|
|
|
|
Ok(WorkspaceDirLintConfig {
|
|
plugins,
|
|
rules,
|
|
files,
|
|
})
|
|
}
|
|
|
|
pub fn to_fmt_config(
|
|
&self,
|
|
cli_args: FilePatterns,
|
|
) -> Result<FmtConfig, ToInvalidConfigError> {
|
|
let mut config = self.to_fmt_config_inner()?;
|
|
self.exclude_includes_with_member_for_base_for_root(&mut config.files);
|
|
combine_files_config_with_cli_args(&mut config.files, cli_args);
|
|
self.append_workspace_members_to_exclude(&mut config.files);
|
|
Ok(config)
|
|
}
|
|
|
|
fn to_fmt_config_inner(&self) -> Result<FmtConfig, ToInvalidConfigError> {
|
|
let Some(deno_json) = self.deno_json.as_ref() else {
|
|
return Ok(FmtConfig {
|
|
files: FilePatterns::new_with_base(
|
|
url_to_file_path(&self.dir_url).unwrap(),
|
|
),
|
|
options: Default::default(),
|
|
});
|
|
};
|
|
let member_config = deno_json.member.to_fmt_config()?;
|
|
let root_config = match &deno_json.root {
|
|
Some(root) => root.to_fmt_config()?,
|
|
None => return Ok(member_config),
|
|
};
|
|
|
|
Ok(FmtConfig {
|
|
options: FmtOptionsConfig {
|
|
use_tabs: member_config
|
|
.options
|
|
.use_tabs
|
|
.or(root_config.options.use_tabs),
|
|
line_width: member_config
|
|
.options
|
|
.line_width
|
|
.or(root_config.options.line_width),
|
|
indent_width: member_config
|
|
.options
|
|
.indent_width
|
|
.or(root_config.options.indent_width),
|
|
single_quote: member_config
|
|
.options
|
|
.single_quote
|
|
.or(root_config.options.single_quote),
|
|
prose_wrap: member_config
|
|
.options
|
|
.prose_wrap
|
|
.or(root_config.options.prose_wrap),
|
|
semi_colons: member_config
|
|
.options
|
|
.semi_colons
|
|
.or(root_config.options.semi_colons),
|
|
quote_props: member_config
|
|
.options
|
|
.quote_props
|
|
.or(root_config.options.quote_props),
|
|
new_line_kind: member_config
|
|
.options
|
|
.new_line_kind
|
|
.or(root_config.options.new_line_kind),
|
|
use_braces: member_config
|
|
.options
|
|
.use_braces
|
|
.or(root_config.options.use_braces),
|
|
brace_position: member_config
|
|
.options
|
|
.brace_position
|
|
.or(root_config.options.brace_position),
|
|
single_body_position: member_config
|
|
.options
|
|
.single_body_position
|
|
.or(root_config.options.single_body_position),
|
|
next_control_flow_position: member_config
|
|
.options
|
|
.next_control_flow_position
|
|
.or(root_config.options.next_control_flow_position),
|
|
trailing_commas: member_config
|
|
.options
|
|
.trailing_commas
|
|
.or(root_config.options.trailing_commas),
|
|
operator_position: member_config
|
|
.options
|
|
.operator_position
|
|
.or(root_config.options.operator_position),
|
|
jsx_bracket_position: member_config
|
|
.options
|
|
.jsx_bracket_position
|
|
.or(root_config.options.jsx_bracket_position),
|
|
jsx_force_new_lines_surrounding_content: member_config
|
|
.options
|
|
.jsx_force_new_lines_surrounding_content
|
|
.or(root_config.options.jsx_force_new_lines_surrounding_content),
|
|
jsx_multi_line_parens: member_config
|
|
.options
|
|
.jsx_multi_line_parens
|
|
.or(root_config.options.jsx_multi_line_parens),
|
|
type_literal_separator_kind: member_config
|
|
.options
|
|
.type_literal_separator_kind
|
|
.or(root_config.options.type_literal_separator_kind),
|
|
space_around: member_config
|
|
.options
|
|
.space_around
|
|
.or(root_config.options.space_around),
|
|
space_surrounding_properties: member_config
|
|
.options
|
|
.space_surrounding_properties
|
|
.or(root_config.options.space_surrounding_properties),
|
|
},
|
|
files: combine_patterns(root_config.files, member_config.files),
|
|
})
|
|
}
|
|
|
|
pub fn to_bench_config(
|
|
&self,
|
|
cli_args: FilePatterns,
|
|
) -> Result<BenchConfig, ToInvalidConfigError> {
|
|
let mut config = self.to_bench_config_inner()?.clone();
|
|
self.exclude_includes_with_member_for_base_for_root(&mut config.files);
|
|
combine_files_config_with_cli_args(&mut config.files, cli_args);
|
|
self.append_workspace_members_to_exclude(&mut config.files);
|
|
Ok(config)
|
|
}
|
|
|
|
fn to_bench_config_inner(
|
|
&self,
|
|
) -> Result<&BenchConfig, ToInvalidConfigError> {
|
|
if let Some(config) = self.cached.bench.get() {
|
|
Ok(config)
|
|
} else {
|
|
let config = self.to_bench_config_inner_no_cache()?;
|
|
_ = self.cached.bench.set(config);
|
|
Ok(self.cached.bench.get().unwrap())
|
|
}
|
|
}
|
|
|
|
fn to_bench_config_inner_no_cache(
|
|
&self,
|
|
) -> Result<BenchConfig, ToInvalidConfigError> {
|
|
let Some(deno_json) = self.deno_json.as_ref() else {
|
|
return Ok(BenchConfig {
|
|
files: FilePatterns::new_with_base(
|
|
url_to_file_path(&self.dir_url).unwrap(),
|
|
),
|
|
permissions: None,
|
|
});
|
|
};
|
|
let permissions = self.to_permissions_config()?;
|
|
let member_config = deno_json.member.to_bench_config(permissions)?;
|
|
let root_config = match &deno_json.root {
|
|
Some(root) => root.to_bench_config(permissions)?,
|
|
None => return Ok(member_config),
|
|
};
|
|
Ok(BenchConfig {
|
|
files: combine_patterns(root_config.files, member_config.files),
|
|
permissions: match (root_config.permissions, member_config.permissions) {
|
|
(_, Some(m)) => Some(m),
|
|
(Some(r), _) => Some(r),
|
|
(None, None) => None,
|
|
},
|
|
})
|
|
}
|
|
|
|
pub fn to_compile_config(
|
|
&self,
|
|
) -> Result<&CompileConfig, ToInvalidConfigError> {
|
|
if let Some(config) = &self.cached.compile.get() {
|
|
Ok(config)
|
|
} else {
|
|
let config = self.to_compile_config_no_cache()?;
|
|
_ = self.cached.compile.set(config);
|
|
Ok(self.cached.compile.get().unwrap())
|
|
}
|
|
}
|
|
|
|
fn to_compile_config_no_cache(
|
|
&self,
|
|
) -> Result<CompileConfig, ToInvalidConfigError> {
|
|
let Some(deno_json) = self.deno_json.as_ref() else {
|
|
return Ok(CompileConfig { permissions: None });
|
|
};
|
|
let permissions = self.to_permissions_config()?;
|
|
let member_config = deno_json.member.to_compile_config(permissions)?;
|
|
let root_config = match &deno_json.root {
|
|
Some(root) => root.to_compile_config(permissions)?,
|
|
None => return Ok(member_config),
|
|
};
|
|
Ok(CompileConfig {
|
|
permissions: match (root_config.permissions, member_config.permissions) {
|
|
(_, Some(m)) => Some(m),
|
|
(Some(r), _) => Some(r),
|
|
(None, None) => None,
|
|
},
|
|
})
|
|
}
|
|
|
|
pub fn to_tasks_config(
|
|
&self,
|
|
) -> Result<WorkspaceTasksConfig, ToTasksConfigError> {
|
|
fn to_member_tasks_config(
|
|
maybe_deno_json: Option<&ConfigFileRc>,
|
|
maybe_pkg_json: Option<&PackageJsonRc>,
|
|
) -> Result<Option<WorkspaceMemberTasksConfig>, ToTasksConfigError> {
|
|
let config = WorkspaceMemberTasksConfig {
|
|
deno_json: match maybe_deno_json {
|
|
Some(deno_json) => deno_json
|
|
.to_tasks_config()
|
|
.map(|tasks| {
|
|
tasks.map(|tasks| WorkspaceMemberTasksConfigFile {
|
|
folder_url: url_parent(&deno_json.specifier),
|
|
tasks,
|
|
package_name: deno_json.json.name.clone(),
|
|
})
|
|
})
|
|
.map_err(|error| ToTasksConfigError {
|
|
specifier: deno_json.specifier.clone(),
|
|
error,
|
|
})?,
|
|
None => None,
|
|
},
|
|
package_json: match maybe_pkg_json {
|
|
Some(pkg_json) => pkg_json.scripts.clone().map(|scripts| {
|
|
WorkspaceMemberTasksConfigFile {
|
|
folder_url: url_parent(&pkg_json.specifier()),
|
|
tasks: scripts,
|
|
package_name: pkg_json.name.clone(),
|
|
}
|
|
}),
|
|
None => None,
|
|
},
|
|
};
|
|
if config.deno_json.is_none() && config.package_json.is_none() {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(config))
|
|
}
|
|
|
|
Ok(WorkspaceTasksConfig {
|
|
root: to_member_tasks_config(
|
|
self.deno_json.as_ref().and_then(|d| d.root.as_ref()),
|
|
self.pkg_json.as_ref().and_then(|d| d.root.as_ref()),
|
|
)?,
|
|
member: to_member_tasks_config(
|
|
self.deno_json.as_ref().map(|d| &d.member),
|
|
self.pkg_json.as_ref().map(|d| &d.member),
|
|
)?,
|
|
})
|
|
}
|
|
|
|
pub fn to_permissions_config(
|
|
&self,
|
|
) -> Result<&PermissionsConfig, ToInvalidConfigError> {
|
|
if let Some(value) = self.cached.permissions.get() {
|
|
Ok(value)
|
|
} else {
|
|
let base = match self.deno_json.as_ref().and_then(|c| c.root.as_ref()) {
|
|
Some(value) => value.to_permissions_config()?,
|
|
None => Default::default(),
|
|
};
|
|
let member = match self.deno_json.as_ref().map(|c| &c.member) {
|
|
Some(value) => value.to_permissions_config()?,
|
|
None => Default::default(),
|
|
};
|
|
let value = base.merge(member);
|
|
_ = self.cached.permissions.set(value);
|
|
Ok(self.cached.permissions.get().unwrap())
|
|
}
|
|
}
|
|
|
|
pub fn to_bench_permissions_config(
|
|
&self,
|
|
) -> Result<Option<&PermissionsObjectWithBase>, ToInvalidConfigError> {
|
|
Ok(self.to_bench_config_inner()?.permissions.as_deref())
|
|
}
|
|
|
|
pub fn to_compile_permissions_config(
|
|
&self,
|
|
) -> Result<Option<&PermissionsObjectWithBase>, ToInvalidConfigError> {
|
|
Ok(self.to_compile_config()?.permissions.as_deref())
|
|
}
|
|
|
|
pub fn to_test_permissions_config(
|
|
&self,
|
|
) -> Result<Option<&PermissionsObjectWithBase>, ToInvalidConfigError> {
|
|
Ok(self.to_test_config_inner()?.permissions.as_deref())
|
|
}
|
|
|
|
pub fn to_publish_config(
|
|
&self,
|
|
) -> Result<PublishConfig, ToInvalidConfigError> {
|
|
let mut config = self.to_publish_config_inner()?;
|
|
self.exclude_includes_with_member_for_base_for_root(&mut config.files);
|
|
self.append_workspace_members_to_exclude(&mut config.files);
|
|
Ok(config)
|
|
}
|
|
|
|
fn to_publish_config_inner(
|
|
&self,
|
|
) -> Result<PublishConfig, ToInvalidConfigError> {
|
|
let Some(deno_json) = self.deno_json.as_ref() else {
|
|
return Ok(PublishConfig {
|
|
files: FilePatterns::new_with_base(
|
|
url_to_file_path(&self.dir_url).unwrap(),
|
|
),
|
|
});
|
|
};
|
|
let member_config = deno_json.member.to_publish_config()?;
|
|
let root_config = match &deno_json.root {
|
|
Some(root) => root.to_publish_config()?,
|
|
None => return Ok(member_config),
|
|
};
|
|
Ok(PublishConfig {
|
|
files: combine_patterns(root_config.files, member_config.files),
|
|
})
|
|
}
|
|
|
|
pub fn to_test_config(
|
|
&self,
|
|
cli_args: FilePatterns,
|
|
) -> Result<TestConfig, ToInvalidConfigError> {
|
|
let mut config = self.to_test_config_inner()?.clone();
|
|
self.exclude_includes_with_member_for_base_for_root(&mut config.files);
|
|
combine_files_config_with_cli_args(&mut config.files, cli_args);
|
|
self.append_workspace_members_to_exclude(&mut config.files);
|
|
Ok(config)
|
|
}
|
|
|
|
fn to_test_config_inner(&self) -> Result<&TestConfig, ToInvalidConfigError> {
|
|
if let Some(config) = self.cached.test.get() {
|
|
Ok(config)
|
|
} else {
|
|
let value = self.to_test_config_inner_no_cache()?;
|
|
_ = self.cached.test.set(value);
|
|
Ok(self.cached.test.get().unwrap())
|
|
}
|
|
}
|
|
|
|
fn to_test_config_inner_no_cache(
|
|
&self,
|
|
) -> Result<TestConfig, ToInvalidConfigError> {
|
|
let Some(deno_json) = self.deno_json.as_ref() else {
|
|
return Ok(TestConfig {
|
|
files: FilePatterns::new_with_base(
|
|
url_to_file_path(&self.dir_url).unwrap(),
|
|
),
|
|
permissions: None,
|
|
});
|
|
};
|
|
let permissions = self.to_permissions_config()?;
|
|
let member_config = deno_json.member.to_test_config(permissions)?;
|
|
let root_config = match &deno_json.root {
|
|
Some(root) => root.to_test_config(permissions)?,
|
|
None => return Ok(member_config),
|
|
};
|
|
|
|
Ok(TestConfig {
|
|
files: combine_patterns(root_config.files, member_config.files),
|
|
permissions: match (root_config.permissions, member_config.permissions) {
|
|
(_, Some(m)) => Some(m),
|
|
(Some(r), _) => Some(r),
|
|
(None, None) => None,
|
|
},
|
|
})
|
|
}
|
|
|
|
pub fn to_deploy_config(
|
|
&self,
|
|
) -> Result<Option<DeployConfig>, ToInvalidConfigError> {
|
|
let config = if let Some(deno_json) = self.deno_json.as_ref() {
|
|
if let Some(config) = deno_json.member.to_deploy_config()? {
|
|
Some(config)
|
|
} else {
|
|
match &deno_json.root {
|
|
Some(root) => root.to_deploy_config()?,
|
|
None => None,
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(config)
|
|
}
|
|
|
|
/// Removes any "include" patterns from the root files that have
|
|
/// a base in another workspace member.
|
|
fn exclude_includes_with_member_for_base_for_root(
|
|
&self,
|
|
files: &mut FilePatterns,
|
|
) {
|
|
let Some(include) = &mut files.include else {
|
|
return;
|
|
};
|
|
let root_url = self.workspace.root_dir_url();
|
|
if self.dir_url != *root_url {
|
|
return; // only do this for the root config
|
|
}
|
|
|
|
let root_folder_configs = self.workspace.root_folder_configs();
|
|
let maybe_root_deno_json = root_folder_configs.deno_json.as_ref();
|
|
let non_root_deno_jsons = match maybe_root_deno_json {
|
|
Some(root_deno_json) => self
|
|
.workspace
|
|
.deno_jsons()
|
|
.filter(|d| d.specifier != root_deno_json.specifier)
|
|
.collect::<Vec<_>>(),
|
|
None => self.workspace.deno_jsons().collect::<Vec<_>>(),
|
|
};
|
|
|
|
let include = include.inner_mut();
|
|
for i in (0..include.len()).rev() {
|
|
let Some(path) = include[i].base_path() else {
|
|
continue;
|
|
};
|
|
for deno_json in non_root_deno_jsons.iter() {
|
|
if path.starts_with(deno_json.dir_path()) {
|
|
include.remove(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn append_workspace_members_to_exclude(&self, files: &mut FilePatterns) {
|
|
files.exclude.append(
|
|
self
|
|
.workspace
|
|
.deno_jsons()
|
|
.filter(|member_deno_json| {
|
|
let member_dir = member_deno_json.dir_path();
|
|
member_dir != files.base && member_dir.starts_with(&files.base)
|
|
})
|
|
.map(|d| PathOrPattern::Path(d.dir_path())),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub enum TaskOrScript<'a> {
|
|
/// A task from a deno.json.
|
|
Task {
|
|
details: &'a WorkspaceMemberTasksConfigFile<TaskDefinition>,
|
|
task: &'a TaskDefinition,
|
|
},
|
|
/// A script from a package.json.
|
|
Script {
|
|
details: &'a WorkspaceMemberTasksConfigFile<String>,
|
|
task: &'a str,
|
|
},
|
|
}
|
|
|
|
impl<'a> TaskOrScript<'a> {
|
|
pub fn package_name(&self) -> Option<&'a str> {
|
|
match self {
|
|
TaskOrScript::Task { details, .. } => details.package_name.as_deref(),
|
|
TaskOrScript::Script { details, .. } => details.package_name.as_deref(),
|
|
}
|
|
}
|
|
|
|
pub fn folder_url(&self) -> &'a Url {
|
|
match self {
|
|
TaskOrScript::Task { details, .. } => &details.folder_url,
|
|
TaskOrScript::Script { details, .. } => &details.folder_url,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct WorkspaceMemberTasksConfigFile<TValue> {
|
|
pub package_name: Option<String>,
|
|
pub folder_url: Url,
|
|
pub tasks: IndexMap<String, TValue>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct WorkspaceMemberTasksConfig {
|
|
pub deno_json: Option<WorkspaceMemberTasksConfigFile<TaskDefinition>>,
|
|
pub package_json: Option<WorkspaceMemberTasksConfigFile<String>>,
|
|
}
|
|
|
|
impl WorkspaceMemberTasksConfig {
|
|
pub fn with_only_pkg_json(self) -> Self {
|
|
WorkspaceMemberTasksConfig {
|
|
deno_json: None,
|
|
package_json: self.package_json,
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self
|
|
.deno_json
|
|
.as_ref()
|
|
.map(|d| d.tasks.is_empty())
|
|
.unwrap_or(true)
|
|
&& self
|
|
.package_json
|
|
.as_ref()
|
|
.map(|d| d.tasks.is_empty())
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
pub fn task_names(&self) -> impl Iterator<Item = &str> {
|
|
self
|
|
.deno_json
|
|
.as_ref()
|
|
.into_iter()
|
|
.flat_map(|d| d.tasks.keys())
|
|
.chain(
|
|
self
|
|
.package_json
|
|
.as_ref()
|
|
.into_iter()
|
|
.flat_map(|d| d.tasks.keys())
|
|
.filter(|pkg_json_key| {
|
|
self
|
|
.deno_json
|
|
.as_ref()
|
|
.map(|d| !d.tasks.contains_key(pkg_json_key.as_str()))
|
|
.unwrap_or(true)
|
|
}),
|
|
)
|
|
.map(|s| s.as_str())
|
|
}
|
|
|
|
pub fn tasks_count(&self) -> usize {
|
|
self.deno_json.as_ref().map(|d| d.tasks.len()).unwrap_or(0)
|
|
+ self
|
|
.package_json
|
|
.as_ref()
|
|
.map(|d| d.tasks.len())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
pub fn task(&self, name: &str) -> Option<TaskOrScript<'_>> {
|
|
self
|
|
.deno_json
|
|
.as_ref()
|
|
.and_then(|config| {
|
|
config.tasks.get(name).map(|task| TaskOrScript::Task {
|
|
details: config,
|
|
task,
|
|
})
|
|
})
|
|
.or_else(|| {
|
|
self.package_json.as_ref().and_then(|config| {
|
|
config.tasks.get(name).map(|script| TaskOrScript::Script {
|
|
details: config,
|
|
task: script,
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct WorkspaceTasksConfig {
|
|
pub root: Option<WorkspaceMemberTasksConfig>,
|
|
pub member: Option<WorkspaceMemberTasksConfig>,
|
|
}
|
|
|
|
impl WorkspaceTasksConfig {
|
|
pub fn with_only_pkg_json(self) -> Self {
|
|
WorkspaceTasksConfig {
|
|
root: self.root.map(|c| c.with_only_pkg_json()),
|
|
member: self.member.map(|c| c.with_only_pkg_json()),
|
|
}
|
|
}
|
|
|
|
pub fn task_names(&self) -> impl Iterator<Item = &str> {
|
|
self
|
|
.member
|
|
.as_ref()
|
|
.into_iter()
|
|
.flat_map(|r| r.task_names())
|
|
.chain(
|
|
self
|
|
.root
|
|
.as_ref()
|
|
.into_iter()
|
|
.flat_map(|m| m.task_names())
|
|
.filter(|root_key| {
|
|
self
|
|
.member
|
|
.as_ref()
|
|
.map(|m| m.task(root_key).is_none())
|
|
.unwrap_or(true)
|
|
}),
|
|
)
|
|
}
|
|
|
|
pub fn task(&self, name: &str) -> Option<TaskOrScript<'_>> {
|
|
self
|
|
.member
|
|
.as_ref()
|
|
.and_then(|m| m.task(name))
|
|
.or_else(|| self.root.as_ref().and_then(|r| r.task(name)))
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.root.as_ref().map(|r| r.is_empty()).unwrap_or(true)
|
|
&& self.member.as_ref().map(|r| r.is_empty()).unwrap_or(true)
|
|
}
|
|
|
|
pub fn tasks_count(&self) -> usize {
|
|
self.root.as_ref().map(|r| r.tasks_count()).unwrap_or(0)
|
|
+ self.member.as_ref().map(|r| r.tasks_count()).unwrap_or(0)
|
|
}
|
|
}
|
|
|
|
fn combine_patterns(
|
|
root_patterns: FilePatterns,
|
|
member_patterns: FilePatterns,
|
|
) -> FilePatterns {
|
|
FilePatterns {
|
|
include: {
|
|
match root_patterns.include {
|
|
Some(root) => {
|
|
let filtered_root =
|
|
root.into_path_or_patterns().into_iter().filter(|p| {
|
|
match p.base_path() {
|
|
Some(base) => base.starts_with(&member_patterns.base),
|
|
None => true,
|
|
}
|
|
});
|
|
match member_patterns.include {
|
|
Some(member) => Some(
|
|
filtered_root
|
|
.chain(member.into_path_or_patterns())
|
|
.collect(),
|
|
),
|
|
None => {
|
|
let matching_root = filtered_root.collect::<Vec<_>>();
|
|
if matching_root.is_empty() {
|
|
// member was None and nothing in the root include list
|
|
// has a base within this member, so use None to discover
|
|
// files in here
|
|
None
|
|
} else {
|
|
Some(matching_root)
|
|
}
|
|
}
|
|
}
|
|
.map(PathOrPatternSet::new)
|
|
}
|
|
None => member_patterns.include,
|
|
}
|
|
},
|
|
exclude: {
|
|
// have the root excludes at the front because they're lower priority
|
|
let patterns = root_patterns
|
|
.exclude
|
|
.into_path_or_patterns()
|
|
.into_iter()
|
|
.filter(|p| match p {
|
|
PathOrPattern::Path(path) |
|
|
PathOrPattern::NegatedPath(path) => path.starts_with(&member_patterns.base),
|
|
PathOrPattern::RemoteUrl(_) |
|
|
// always include patterns because they may be something like ./**/*.ts in the root
|
|
PathOrPattern::Pattern(_) => true,
|
|
})
|
|
.chain(member_patterns.exclude.into_path_or_patterns())
|
|
.collect::<Vec<_>>();
|
|
PathOrPatternSet::new(patterns)
|
|
},
|
|
base: member_patterns.base,
|
|
}
|
|
}
|
|
|
|
fn combine_files_config_with_cli_args(
|
|
files_config: &mut FilePatterns,
|
|
cli_arg_patterns: FilePatterns,
|
|
) {
|
|
if cli_arg_patterns.base.starts_with(&files_config.base)
|
|
|| !files_config.base.starts_with(&cli_arg_patterns.base)
|
|
{
|
|
files_config.base = cli_arg_patterns.base;
|
|
}
|
|
if let Some(include) = cli_arg_patterns.include
|
|
&& !include.inner().is_empty()
|
|
{
|
|
files_config.include = Some(include);
|
|
}
|
|
if !cli_arg_patterns.exclude.inner().is_empty() {
|
|
files_config.exclude = cli_arg_patterns.exclude;
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::owned_cow)]
|
|
struct CombineOptionVecsWithOverride<'a, T: Clone> {
|
|
root: Option<Vec<T>>,
|
|
member: Option<Cow<'a, Vec<T>>>,
|
|
member_override_root: Option<&'a Vec<T>>,
|
|
}
|
|
|
|
fn combine_option_vecs_with_override<T: Eq + std::hash::Hash + Clone>(
|
|
opts: CombineOptionVecsWithOverride<T>,
|
|
) -> Option<Vec<T>> {
|
|
let root = opts.root.map(|r| {
|
|
let member_override_root = opts
|
|
.member_override_root
|
|
.map(|p| p.iter().collect::<HashSet<_>>())
|
|
.unwrap_or_default();
|
|
r.into_iter()
|
|
.filter(|p| !member_override_root.contains(p))
|
|
.collect::<Vec<_>>()
|
|
});
|
|
match (root, opts.member) {
|
|
(Some(root), Some(member)) => {
|
|
let capacity = root.len() + member.len();
|
|
Some(match member {
|
|
Cow::Owned(m) => {
|
|
remove_duplicates_iterator(root.into_iter().chain(m), capacity)
|
|
}
|
|
Cow::Borrowed(m) => remove_duplicates_iterator(
|
|
root.into_iter().chain(m.iter().map(|c| (*c).clone())),
|
|
capacity,
|
|
),
|
|
})
|
|
}
|
|
(Some(root), None) => Some(root),
|
|
(None, Some(member)) => Some(match member {
|
|
Cow::Owned(m) => m,
|
|
Cow::Borrowed(m) => m.iter().map(|c| (*c).clone()).collect(),
|
|
}),
|
|
(None, None) => None,
|
|
}
|
|
}
|
|
|
|
fn combine_option_vecs<T: Eq + std::hash::Hash + Clone>(
|
|
root_option: Option<Vec<T>>,
|
|
member_option: Option<Vec<T>>,
|
|
) -> Option<Vec<T>> {
|
|
match (root_option, member_option) {
|
|
(Some(root), Some(member)) => {
|
|
if root.is_empty() {
|
|
return Some(member);
|
|
}
|
|
if member.is_empty() {
|
|
return Some(root);
|
|
}
|
|
let capacity = root.len() + member.len();
|
|
Some(remove_duplicates_iterator(
|
|
root.into_iter().chain(member),
|
|
capacity,
|
|
))
|
|
}
|
|
(Some(root), None) => Some(root),
|
|
(None, Some(member)) => Some(member),
|
|
(None, None) => None,
|
|
}
|
|
}
|
|
|
|
fn remove_duplicates_iterator<T: Eq + std::hash::Hash + Clone>(
|
|
iterator: impl IntoIterator<Item = T>,
|
|
capacity: usize,
|
|
) -> Vec<T> {
|
|
let mut seen = HashSet::with_capacity(capacity);
|
|
let mut result = Vec::with_capacity(capacity);
|
|
for item in iterator {
|
|
if seen.insert(item.clone()) {
|
|
result.push(item);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn parent_specifier_str(specifier: &str) -> Option<&str> {
|
|
let specifier = specifier.strip_suffix('/').unwrap_or(specifier);
|
|
if let Some(index) = specifier.rfind('/') {
|
|
Some(&specifier[..index + 1])
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn is_valid_jsr_pkg_name(name: &str) -> bool {
|
|
let jsr = deno_semver::jsr::JsrPackageReqReference::from_str(&format!(
|
|
"jsr:{}@*",
|
|
name
|
|
));
|
|
match jsr {
|
|
Ok(jsr) => jsr.sub_path().is_none(),
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub mod test {
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
|
|
use deno_path_util::normalize_path;
|
|
use deno_path_util::url_from_directory_path;
|
|
use deno_path_util::url_from_file_path;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use sys_traits::impls::InMemorySys;
|
|
|
|
use super::*;
|
|
use crate::assert_contains;
|
|
use crate::deno_json::BracePosition;
|
|
use crate::deno_json::BracketPosition;
|
|
use crate::deno_json::DenoJsonCache;
|
|
use crate::deno_json::MultiLineParens;
|
|
use crate::deno_json::NewLineKind;
|
|
use crate::deno_json::NextControlFlowPosition;
|
|
use crate::deno_json::OperatorPosition;
|
|
use crate::deno_json::ProseWrap;
|
|
use crate::deno_json::QuoteProps;
|
|
use crate::deno_json::SeparatorKind;
|
|
use crate::deno_json::SingleBodyPosition;
|
|
use crate::deno_json::TrailingCommas;
|
|
use crate::deno_json::UseBraces;
|
|
use crate::glob::FileCollector;
|
|
use crate::glob::GlobPattern;
|
|
use crate::glob::PathKind;
|
|
use crate::glob::PathOrPattern;
|
|
|
|
pub struct UnreachableSys;
|
|
|
|
impl sys_traits::BaseFsMetadata for UnreachableSys {
|
|
type Metadata = sys_traits::impls::RealFsMetadata;
|
|
|
|
#[doc(hidden)]
|
|
fn base_fs_metadata(
|
|
&self,
|
|
_path: &Path,
|
|
) -> std::io::Result<Self::Metadata> {
|
|
unreachable!()
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn base_fs_symlink_metadata(
|
|
&self,
|
|
_path: &Path,
|
|
) -> std::io::Result<Self::Metadata> {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
impl sys_traits::BaseFsRead for UnreachableSys {
|
|
fn base_fs_read(
|
|
&self,
|
|
_path: &Path,
|
|
) -> std::io::Result<Cow<'static, [u8]>> {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
fn root_dir() -> PathBuf {
|
|
if cfg!(windows) {
|
|
PathBuf::from("C:\\Users\\user")
|
|
} else {
|
|
PathBuf::from("/home/user")
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_workspaces() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": []
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("sub_dir").join("deno.json"),
|
|
json!({
|
|
"workspace": []
|
|
}),
|
|
);
|
|
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir().join("sub_dir")]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.deno_jsons()
|
|
.map(|d| d.specifier.to_file_path().unwrap())
|
|
.collect::<Vec<_>>(),
|
|
vec![root_dir().join("sub_dir").join("deno.json")]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_members() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member/a", "./member/../member/a"],
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/a/deno.json"), json!({}));
|
|
|
|
let workspace_config_err = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.err()
|
|
.unwrap();
|
|
|
|
assert_contains!(
|
|
workspace_config_err.to_string(),
|
|
"Cannot specify a workspace member twice ('./member/../member/a')."
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_workspace_invalid_self_reference() {
|
|
for reference in [".", "../sub_dir"] {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("sub_dir").join("deno.json"),
|
|
json!({
|
|
"workspace": [reference],
|
|
}),
|
|
);
|
|
|
|
let workspace_config_err = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir().join("sub_dir")]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.err()
|
|
.unwrap();
|
|
|
|
assert_contains!(
|
|
workspace_config_err.to_string(),
|
|
&format!(
|
|
"Remove the reference to the current config file (\"{reference}\") in \"workspaces\"."
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_workspaces_outside_root_config_dir() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["../a"]
|
|
}),
|
|
);
|
|
|
|
let workspace_config_err = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.err()
|
|
.unwrap();
|
|
|
|
assert_contains!(
|
|
workspace_config_err.to_string(),
|
|
"Workspace member must be nested in a directory under the workspace."
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_workspaces_json_jsonc() {
|
|
let sys = InMemorySys::default();
|
|
let config_text = json!({
|
|
"workspace": [
|
|
"./a",
|
|
"./b",
|
|
],
|
|
});
|
|
let config_text_a = json!({
|
|
"name": "a",
|
|
"version": "0.1.0"
|
|
});
|
|
let config_text_b = json!({
|
|
"name": "b",
|
|
"version": "0.2.0"
|
|
});
|
|
|
|
sys.fs_insert_json(root_dir().join("deno.json"), config_text);
|
|
sys.fs_insert_json(root_dir().join("a/deno.json"), config_text_a);
|
|
sys.fs_insert_json(root_dir().join("b/deno.jsonc"), config_text_b);
|
|
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(workspace_dir.workspace.config_folders.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tasks() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member", "./pkg_json"],
|
|
"tasks": {
|
|
"hi": "echo hi",
|
|
"overwrite": "echo overwrite"
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({
|
|
"tasks": {
|
|
"overwrite": "echo overwritten",
|
|
"bye": "echo bye"
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("pkg_json/package.json"),
|
|
json!({
|
|
"scripts": {
|
|
"script": "echo 1"
|
|
}
|
|
}),
|
|
);
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
// start at root for this test
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let root_deno_json = Some(WorkspaceMemberTasksConfigFile {
|
|
folder_url: url_from_directory_path(&root_dir()).unwrap(),
|
|
package_name: None,
|
|
tasks: IndexMap::from([
|
|
("hi".to_string(), "echo hi".into()),
|
|
("overwrite".to_string(), "echo overwrite".into()),
|
|
]),
|
|
});
|
|
let root = Some(WorkspaceMemberTasksConfig {
|
|
deno_json: root_deno_json.clone(),
|
|
package_json: None,
|
|
});
|
|
// root
|
|
{
|
|
let tasks_config = workspace_dir.to_tasks_config().unwrap();
|
|
assert_eq!(
|
|
tasks_config,
|
|
WorkspaceTasksConfig {
|
|
root: None,
|
|
// the root context will have the root config as the member config
|
|
member: root.clone(),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
tasks_config.task_names().collect::<Vec<_>>(),
|
|
["hi", "overwrite"]
|
|
);
|
|
}
|
|
// member
|
|
{
|
|
let member_dir = workspace_dir.workspace.resolve_member_dir(
|
|
&url_from_directory_path(&root_dir().join("member/deno.json")).unwrap(),
|
|
);
|
|
let tasks_config = member_dir.to_tasks_config().unwrap();
|
|
assert_eq!(
|
|
tasks_config,
|
|
WorkspaceTasksConfig {
|
|
root: root.clone(),
|
|
member: Some(WorkspaceMemberTasksConfig {
|
|
deno_json: Some(WorkspaceMemberTasksConfigFile {
|
|
folder_url: url_from_directory_path(&root_dir().join("member"))
|
|
.unwrap(),
|
|
package_name: None,
|
|
tasks: IndexMap::from([
|
|
("overwrite".to_string(), "echo overwritten".into()),
|
|
("bye".to_string(), "echo bye".into()),
|
|
]),
|
|
}),
|
|
package_json: None,
|
|
}),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
tasks_config.task_names().collect::<Vec<_>>(),
|
|
["overwrite", "bye", "hi"]
|
|
);
|
|
}
|
|
// pkg json
|
|
{
|
|
let member_dir = workspace_dir.workspace.resolve_member_dir(
|
|
&url_from_directory_path(&root_dir().join("pkg_json/package.json"))
|
|
.unwrap(),
|
|
);
|
|
let tasks_config = member_dir.to_tasks_config().unwrap();
|
|
assert_eq!(
|
|
tasks_config,
|
|
WorkspaceTasksConfig {
|
|
root: None,
|
|
member: Some(WorkspaceMemberTasksConfig {
|
|
deno_json: root_deno_json.clone(),
|
|
package_json: Some(WorkspaceMemberTasksConfigFile {
|
|
folder_url: url_from_directory_path(&root_dir().join("pkg_json"))
|
|
.unwrap(),
|
|
package_name: None,
|
|
tasks: IndexMap::from([(
|
|
"script".to_string(),
|
|
"echo 1".to_string()
|
|
)]),
|
|
}),
|
|
})
|
|
}
|
|
);
|
|
assert_eq!(
|
|
tasks_config.task_names().collect::<Vec<_>>(),
|
|
["hi", "overwrite", "script"]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_import_map() {
|
|
let workspace_dir = workspace_for_root_and_member_with_fs(
|
|
json!({
|
|
"importMap": "./other.json",
|
|
}),
|
|
json!({
|
|
"importMap": "./member.json",
|
|
}),
|
|
|fs| {
|
|
fs.fs_insert_json(root_dir().join("other.json"), json!({}));
|
|
fs.fs_insert_json(root_dir().join("member/member.json"), json!({}));
|
|
},
|
|
);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.to_import_map_path()
|
|
.unwrap()
|
|
.unwrap(),
|
|
root_dir().join("other.json"),
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("importMap"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_link() {
|
|
let workspace_dir = workspace_for_root_and_member_with_fs(
|
|
json!({
|
|
"links": ["../dir"],
|
|
}),
|
|
json!({
|
|
"links": [
|
|
"../../dir"
|
|
],
|
|
}),
|
|
|fs| {
|
|
fs.fs_insert_json(root_dir().join("../dir/deno.json"), json!({}));
|
|
},
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("links"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_link_of_link() {
|
|
let workspace_dir = workspace_for_root_and_member_with_fs(
|
|
json!({
|
|
"links": ["../dir"],
|
|
}),
|
|
json!({}),
|
|
|fs| {
|
|
fs.fs_insert_json(
|
|
root_dir().join("../dir/deno.json"),
|
|
json!({
|
|
"links": ["./subdir"] // will be ignored
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("links"),
|
|
config_url: url_from_directory_path(&root_dir())
|
|
.unwrap()
|
|
.join("../dir/deno.json")
|
|
.unwrap(),
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_link_not_exists() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"links": ["./member"]
|
|
}),
|
|
);
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir());
|
|
match err.into_kind() {
|
|
WorkspaceDiscoverErrorKind::ResolveLink { link, base, source } => {
|
|
assert_eq!(link, "./member");
|
|
assert_eq!(base, url_from_directory_path(&root_dir()).unwrap());
|
|
match source.into_kind() {
|
|
ResolveWorkspaceLinkErrorKind::NotFound { dir_url } => {
|
|
assert_eq!(
|
|
dir_url,
|
|
url_from_directory_path(&root_dir().join("member")).unwrap()
|
|
);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_link_workspace_member() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member"],
|
|
"links": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/deno.json"), json!({}));
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir());
|
|
match err.into_kind() {
|
|
WorkspaceDiscoverErrorKind::ResolveLink { link, base, source } => {
|
|
assert_eq!(link, "./member");
|
|
assert_eq!(base, url_from_directory_path(&root_dir()).unwrap());
|
|
assert!(matches!(
|
|
source.into_kind(),
|
|
ResolveWorkspaceLinkErrorKind::WorkspaceMemberNotAllowed
|
|
));
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_link_npm_package() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("pkg/deno.json"),
|
|
json!({
|
|
"links": ["../dir"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("dir/package.json"), json!({}));
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir().join("pkg"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let link_folders = workspace_dir
|
|
.workspace
|
|
.link_folders()
|
|
.values()
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(link_folders.len(), 1);
|
|
assert_eq!(
|
|
link_folders[0].pkg_json.as_ref().unwrap().specifier(),
|
|
url_from_file_path(&root_dir().join("dir/package.json")).unwrap()
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_link_absolute_path() {
|
|
let root_path = root_dir().join("../dir");
|
|
let workspace_dir = workspace_for_root_and_member_with_fs(
|
|
json!({
|
|
"links": [root_path.to_string_lossy().into_owned()],
|
|
}),
|
|
json!({}),
|
|
|fs| {
|
|
fs.fs_insert_json(root_dir().join("../dir/deno.json"), json!({}));
|
|
},
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let link_folders = workspace_dir
|
|
.workspace
|
|
.link_folders()
|
|
.values()
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(link_folders.len(), 1);
|
|
assert_eq!(
|
|
link_folders[0].deno_json.as_ref().unwrap().specifier,
|
|
url_from_file_path(&root_dir().join("../dir/deno.json")).unwrap()
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_imports_and_scopes() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"imports": {
|
|
"@scope/pkg": "jsr:@scope/pkg@1"
|
|
},
|
|
"scopes": {
|
|
"https://deno.land/x/": {
|
|
"@scope/pkg": "jsr:@scope/pkg@2"
|
|
}
|
|
}
|
|
}),
|
|
json!({
|
|
"imports": {
|
|
"@scope/pkg": "jsr:@scope/pkg@3"
|
|
},
|
|
// will ignore this scopes because it's not in the root
|
|
"scopes": {
|
|
"https://deno.land/x/other": {
|
|
"@scope/pkg": "jsr:@scope/pkg@4"
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("scopes"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deprecated_patch() {
|
|
let workspace_dir = workspace_for_root_and_member_with_fs(
|
|
json!({
|
|
"patch": ["../dir"],
|
|
}),
|
|
json!({}),
|
|
|fs| {
|
|
fs.fs_insert_json(root_dir().join("../dir/deno.json"), json!({}));
|
|
},
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::DeprecatedPatch,
|
|
config_url: Url::from_file_path(root_dir().join("deno.json")).unwrap(),
|
|
}]
|
|
);
|
|
assert_eq!(workspace_dir.workspace.link_folders().len(), 1); // should still work though
|
|
}
|
|
|
|
#[test]
|
|
fn test_imports_with_import_map() {
|
|
let workspace_dir = workspace_for_root_and_member_with_fs(
|
|
json!({
|
|
"imports": {},
|
|
"importMap": "./other.json",
|
|
}),
|
|
json!({}),
|
|
|fs| {
|
|
fs.fs_insert_json(root_dir().join("other.json"), json!({}));
|
|
},
|
|
);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.to_import_map_path()
|
|
.unwrap()
|
|
.unwrap(),
|
|
root_dir().join("other.json")
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::ImportMapReferencingImportMap,
|
|
config_url: Url::from_file_path(root_dir().join("deno.json")).unwrap(),
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_import_map_with_member_imports_and_scopes() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"importMap": "./other.json"
|
|
}),
|
|
json!({
|
|
"imports": {
|
|
"@scope/pkg": "jsr:@scope/pkg@3"
|
|
}
|
|
}),
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::MemberImportsScopesIgnored,
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_exclude() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"exclude": [
|
|
"./root",
|
|
"./member/vendor",
|
|
"./**/*.js"
|
|
]
|
|
}),
|
|
json!({
|
|
"exclude": [
|
|
"./member_exclude",
|
|
// unexclude from root
|
|
"!./vendor"
|
|
]
|
|
}),
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let lint_config = workspace_dir
|
|
.to_lint_config(FilePatterns::new_with_base(workspace_dir.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
lint_config.files,
|
|
FilePatterns {
|
|
base: root_dir().join("member"),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(root_dir().join("member").join("vendor")),
|
|
PathOrPattern::Pattern(
|
|
GlobPattern::from_relative(&root_dir(), "./**/*.js").unwrap()
|
|
),
|
|
PathOrPattern::Path(root_dir().join("member").join("member_exclude")),
|
|
PathOrPattern::NegatedPath(root_dir().join("member").join("vendor")),
|
|
]),
|
|
}
|
|
);
|
|
|
|
// will match because it was unexcluded in the member
|
|
assert!(
|
|
lint_config
|
|
.files
|
|
.matches_path(&root_dir().join("member/vendor"), PathKind::Directory)
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_lint_combinations() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"lint": {
|
|
"report": "json",
|
|
"rules": {
|
|
"tags": ["tag1"],
|
|
"include": ["rule1"],
|
|
"exclude": ["rule2"],
|
|
},
|
|
"plugins": ["jsr:@deno/test-plugin1", "jsr:@deno/test-plugin3"]
|
|
}
|
|
}),
|
|
json!({
|
|
"lint": {
|
|
"report": "pretty",
|
|
"include": ["subdir"],
|
|
"rules": {
|
|
"tags": ["tag1"],
|
|
"include": ["rule2"],
|
|
},
|
|
"plugins": [
|
|
"jsr:@deno/test-plugin1",
|
|
"jsr:@deno/test-plugin2",
|
|
"!jsr:@deno/test-plugin3"
|
|
]
|
|
}
|
|
}),
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("lint.report"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.to_lint_config().unwrap(),
|
|
WorkspaceLintConfig {
|
|
report: Some("json".to_string()),
|
|
}
|
|
);
|
|
let lint_config = workspace_dir
|
|
.to_lint_config(FilePatterns::new_with_base(workspace_dir.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
lint_config,
|
|
WorkspaceDirLintConfig {
|
|
rules: LintRulesConfig {
|
|
tags: Some(vec!["tag1".to_string()]),
|
|
include: Some(vec!["rule1".to_string(), "rule2".to_string()]),
|
|
exclude: Some(vec![]),
|
|
},
|
|
plugins: vec![
|
|
Url::parse("jsr:@deno/test-plugin1").unwrap(),
|
|
Url::parse("jsr:@deno/test-plugin2").unwrap(),
|
|
],
|
|
files: FilePatterns {
|
|
base: root_dir().join("member/"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member").join("subdir")
|
|
)])),
|
|
exclude: Default::default(),
|
|
},
|
|
},
|
|
);
|
|
|
|
// check the root context
|
|
let root_ctx = workspace_dir
|
|
.workspace
|
|
.resolve_member_dir(&url_from_directory_path(&root_dir()).unwrap());
|
|
let root_lint_config = root_ctx
|
|
.to_lint_config(FilePatterns::new_with_base(root_ctx.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
root_lint_config,
|
|
WorkspaceDirLintConfig {
|
|
rules: LintRulesConfig {
|
|
tags: Some(vec!["tag1".to_string()]),
|
|
include: Some(vec!["rule1".to_string()]),
|
|
exclude: Some(vec!["rule2".to_string()]),
|
|
},
|
|
plugins: vec![
|
|
Url::parse("jsr:@deno/test-plugin1").unwrap(),
|
|
Url::parse("jsr:@deno/test-plugin3").unwrap(),
|
|
],
|
|
files: FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
// the workspace member will be excluded because that needs
|
|
// to be resolved separately
|
|
exclude: PathOrPatternSet::new(Vec::from([PathOrPattern::Path(
|
|
root_dir().join("member")
|
|
)])),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_fmt_combinations() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"fmt": {
|
|
"useTabs": true,
|
|
"indentWidth": 4,
|
|
"lineWidth": 80,
|
|
"proseWrap": "never",
|
|
"singleQuote": false,
|
|
"semiColons": false,
|
|
"quoteProps": "asNeeded",
|
|
"newLineKind": "auto",
|
|
"useBraces": "preferNone",
|
|
"bracePosition": "maintain",
|
|
"singleBodyPosition": "sameLine",
|
|
"nextControlFlowPosition": "nextLine",
|
|
"trailingCommas": "always",
|
|
"operatorPosition": "sameLine",
|
|
"jsx.bracketPosition": "sameLine",
|
|
"jsx.forceNewLinesSurroundingContent": false,
|
|
"jsx.multiLineParens": "prefer",
|
|
"typeLiteral.separatorKind": "comma",
|
|
"spaceAround": false,
|
|
"spaceSurroundingProperties": false,
|
|
}
|
|
}),
|
|
json!({
|
|
"fmt": {
|
|
"exclude": ["subdir"],
|
|
"useTabs": false,
|
|
"indentWidth": 8,
|
|
"lineWidth": 120,
|
|
"proseWrap": "always",
|
|
"singleQuote": true,
|
|
"semiColons": true,
|
|
"quoteProps": "consistent",
|
|
"newLineKind": "lf",
|
|
"useBraces": "always",
|
|
"bracePosition": "nextLine",
|
|
"singleBodyPosition": "maintain",
|
|
"nextControlFlowPosition": "maintain",
|
|
"trailingCommas": "onlyMultiLine",
|
|
"operatorPosition": "nextLine",
|
|
"jsx.bracketPosition": "nextLine",
|
|
"jsx.forceNewLinesSurroundingContent": true,
|
|
"jsx.multiLineParens": "always",
|
|
"typeLiteral.separatorKind": "semiColon",
|
|
"spaceAround": true,
|
|
"spaceSurroundingProperties": true,
|
|
}
|
|
}),
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let fmt_config = workspace_dir
|
|
.to_fmt_config(FilePatterns::new_with_base(workspace_dir.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
fmt_config,
|
|
FmtConfig {
|
|
options: FmtOptionsConfig {
|
|
use_tabs: Some(false),
|
|
line_width: Some(120),
|
|
indent_width: Some(8),
|
|
prose_wrap: Some(ProseWrap::Always),
|
|
single_quote: Some(true),
|
|
semi_colons: Some(true),
|
|
quote_props: Some(QuoteProps::Consistent),
|
|
new_line_kind: Some(NewLineKind::LineFeed),
|
|
use_braces: Some(UseBraces::Always),
|
|
brace_position: Some(BracePosition::NextLine),
|
|
single_body_position: Some(SingleBodyPosition::Maintain),
|
|
next_control_flow_position: Some(NextControlFlowPosition::Maintain),
|
|
trailing_commas: Some(TrailingCommas::OnlyMultiLine),
|
|
operator_position: Some(OperatorPosition::NextLine),
|
|
jsx_bracket_position: Some(BracketPosition::NextLine),
|
|
jsx_force_new_lines_surrounding_content: Some(true),
|
|
jsx_multi_line_parens: Some(MultiLineParens::Always),
|
|
type_literal_separator_kind: Some(SeparatorKind::SemiColon),
|
|
space_around: Some(true),
|
|
space_surrounding_properties: Some(true),
|
|
},
|
|
files: FilePatterns {
|
|
base: root_dir().join("member"),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member").join("subdir")
|
|
)]),
|
|
},
|
|
}
|
|
);
|
|
|
|
// check the root context
|
|
let root_ctx = workspace_dir
|
|
.workspace
|
|
.resolve_member_dir(&url_from_directory_path(&root_dir()).unwrap());
|
|
let root_fmt_config = root_ctx
|
|
.to_fmt_config(FilePatterns::new_with_base(root_ctx.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
root_fmt_config,
|
|
FmtConfig {
|
|
options: FmtOptionsConfig {
|
|
use_tabs: Some(true),
|
|
line_width: Some(80),
|
|
indent_width: Some(4),
|
|
prose_wrap: Some(ProseWrap::Never),
|
|
single_quote: Some(false),
|
|
semi_colons: Some(false),
|
|
quote_props: Some(QuoteProps::AsNeeded),
|
|
new_line_kind: Some(NewLineKind::Auto),
|
|
use_braces: Some(UseBraces::PreferNone),
|
|
brace_position: Some(BracePosition::Maintain),
|
|
single_body_position: Some(SingleBodyPosition::SameLine),
|
|
next_control_flow_position: Some(NextControlFlowPosition::NextLine),
|
|
trailing_commas: Some(TrailingCommas::Always),
|
|
operator_position: Some(OperatorPosition::SameLine),
|
|
jsx_bracket_position: Some(BracketPosition::SameLine),
|
|
jsx_force_new_lines_surrounding_content: Some(false),
|
|
jsx_multi_line_parens: Some(MultiLineParens::Prefer),
|
|
type_literal_separator_kind: Some(SeparatorKind::Comma),
|
|
space_around: Some(false),
|
|
space_surrounding_properties: Some(false),
|
|
},
|
|
files: FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
// the workspace member will be excluded because that needs
|
|
// to be resolved separately
|
|
exclude: PathOrPatternSet::new(Vec::from([PathOrPattern::Path(
|
|
root_dir().join("member")
|
|
)])),
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_bench_combinations() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({}),
|
|
json!({
|
|
"bench": {
|
|
"exclude": ["subdir"],
|
|
}
|
|
}),
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let bench_config = workspace_dir
|
|
.to_bench_config(FilePatterns::new_with_base(workspace_dir.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
bench_config,
|
|
BenchConfig {
|
|
files: FilePatterns {
|
|
base: root_dir().join("member"),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member").join("subdir")
|
|
)]),
|
|
},
|
|
permissions: None,
|
|
}
|
|
);
|
|
|
|
// check the root context
|
|
let root_ctx = workspace_dir
|
|
.workspace
|
|
.resolve_member_dir(&url_from_directory_path(&root_dir()).unwrap());
|
|
let root_bench_config = root_ctx
|
|
.to_bench_config(FilePatterns::new_with_base(root_ctx.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
root_bench_config,
|
|
BenchConfig {
|
|
files: FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
// the workspace member will be excluded because that needs
|
|
// to be resolved separately
|
|
exclude: PathOrPatternSet::new(Vec::from([PathOrPattern::Path(
|
|
root_dir().join("member")
|
|
)])),
|
|
},
|
|
permissions: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_test_combinations() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({}),
|
|
json!({
|
|
"test": {
|
|
"include": ["subdir"],
|
|
}
|
|
}),
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let config = workspace_dir
|
|
.to_test_config(FilePatterns::new_with_base(workspace_dir.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
config,
|
|
TestConfig {
|
|
files: FilePatterns {
|
|
base: root_dir().join("member"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member").join("subdir")
|
|
)])),
|
|
exclude: Default::default(),
|
|
},
|
|
permissions: None,
|
|
}
|
|
);
|
|
|
|
// check the root context
|
|
let root_ctx = workspace_dir
|
|
.workspace
|
|
.resolve_member_dir(&url_from_directory_path(&root_dir()).unwrap());
|
|
let root_test_config = root_ctx
|
|
.to_test_config(FilePatterns::new_with_base(root_ctx.dir_path()))
|
|
.unwrap();
|
|
assert_eq!(
|
|
root_test_config,
|
|
TestConfig {
|
|
files: FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
// the workspace member will be excluded because that needs
|
|
// to be resolved separately
|
|
exclude: PathOrPatternSet::new(Vec::from([PathOrPattern::Path(
|
|
root_dir().join("member")
|
|
)])),
|
|
},
|
|
permissions: None,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_publish_combinations() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"publish": {
|
|
"exclude": ["other"]
|
|
}
|
|
}),
|
|
json!({
|
|
"publish": {
|
|
"include": ["subdir"],
|
|
},
|
|
"exclude": [
|
|
"./exclude_dir"
|
|
],
|
|
}),
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let config = workspace_dir.to_publish_config().unwrap();
|
|
assert_eq!(
|
|
config,
|
|
PublishConfig {
|
|
files: FilePatterns {
|
|
base: root_dir().join("member"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member").join("subdir")
|
|
)])),
|
|
exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member").join("exclude_dir")
|
|
),]),
|
|
},
|
|
}
|
|
);
|
|
|
|
// check the root context
|
|
let root_publish_config = workspace_dir
|
|
.workspace
|
|
.resolve_member_dir(&url_from_directory_path(&root_dir()).unwrap())
|
|
.to_publish_config()
|
|
.unwrap();
|
|
assert_eq!(
|
|
root_publish_config,
|
|
PublishConfig {
|
|
files: FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(Vec::from([
|
|
PathOrPattern::Path(root_dir().join("other")),
|
|
// the workspace member will be excluded because that needs
|
|
// to be resolved separately
|
|
PathOrPattern::Path(root_dir().join("member")),
|
|
])),
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_empty_config_resolves_excluded_members() {
|
|
let workspace_dir = workspace_for_root_and_member(json!({}), json!({}));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let expected_root_files = FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
// the workspace member will be excluded because that needs
|
|
// to be resolved separately
|
|
exclude: PathOrPatternSet::new(Vec::from([PathOrPattern::Path(
|
|
root_dir().join("member"),
|
|
)])),
|
|
};
|
|
let root_ctx = workspace_dir
|
|
.workspace
|
|
.resolve_member_dir(&url_from_directory_path(&root_dir()).unwrap());
|
|
let expected_member_files = FilePatterns {
|
|
base: root_dir().join("member"),
|
|
include: None,
|
|
exclude: Default::default(),
|
|
};
|
|
|
|
for (expected_files, ctx) in [
|
|
(expected_root_files, root_ctx),
|
|
(expected_member_files, workspace_dir),
|
|
] {
|
|
assert_eq!(
|
|
ctx
|
|
.to_bench_config(FilePatterns::new_with_base(ctx.dir_path()))
|
|
.unwrap(),
|
|
BenchConfig {
|
|
files: expected_files.clone(),
|
|
permissions: None,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
ctx
|
|
.to_fmt_config(FilePatterns::new_with_base(ctx.dir_path()))
|
|
.unwrap(),
|
|
FmtConfig {
|
|
options: Default::default(),
|
|
files: expected_files.clone(),
|
|
}
|
|
);
|
|
assert_eq!(
|
|
ctx
|
|
.to_lint_config(FilePatterns::new_with_base(ctx.dir_path()))
|
|
.unwrap(),
|
|
WorkspaceDirLintConfig {
|
|
rules: Default::default(),
|
|
plugins: Default::default(),
|
|
files: expected_files.clone(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
ctx
|
|
.to_test_config(FilePatterns::new_with_base(ctx.dir_path()))
|
|
.unwrap(),
|
|
TestConfig {
|
|
files: expected_files.clone(),
|
|
permissions: None,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
ctx.to_publish_config().unwrap(),
|
|
PublishConfig {
|
|
files: expected_files.clone(),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_root_only_in_member() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"unstable": ["byonm"],
|
|
"lock": false,
|
|
"nodeModulesDir": false,
|
|
"vendor": true,
|
|
}),
|
|
json!({
|
|
"unstable": ["sloppy-imports"],
|
|
"lock": true,
|
|
"nodeModulesDir": "auto",
|
|
"vendor": false,
|
|
}),
|
|
);
|
|
// ignores member config
|
|
assert_eq!(
|
|
workspace_dir.workspace.unstable_features(),
|
|
&["byonm".to_string()]
|
|
);
|
|
assert!(workspace_dir.workspace.has_unstable("byonm"));
|
|
assert!(!workspace_dir.workspace.has_unstable("sloppy-imports"));
|
|
assert_eq!(
|
|
workspace_dir.workspace.resolve_lockfile_path().unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.node_modules_dir().unwrap(),
|
|
Some(NodeModulesDirMode::None)
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.resolve_lockfile_path().unwrap(),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.vendor_dir_path().unwrap(),
|
|
&root_dir().join("vendor")
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![
|
|
WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::DeprecatedNodeModulesDirOption {
|
|
previous: false,
|
|
suggestion: NodeModulesDirMode::Manual,
|
|
},
|
|
config_url: Url::from_file_path(root_dir().join("deno.json"))
|
|
.unwrap(),
|
|
},
|
|
WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("lock"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
},
|
|
WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("nodeModulesDir"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
},
|
|
WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("unstable"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
},
|
|
WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("vendor"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
},
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_node_modules_dir_suggestions() {
|
|
fn suggest(
|
|
previous: bool,
|
|
suggestion: NodeModulesDirMode,
|
|
) -> WorkspaceDiagnostic {
|
|
WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::DeprecatedNodeModulesDirOption {
|
|
previous,
|
|
suggestion,
|
|
},
|
|
config_url: Url::from_file_path(root_dir().join("deno.json")).unwrap(),
|
|
}
|
|
}
|
|
|
|
let cases = [
|
|
(
|
|
json!({
|
|
"unstable": ["byonm"],
|
|
"nodeModulesDir": true,
|
|
}),
|
|
true,
|
|
NodeModulesDirMode::Manual,
|
|
),
|
|
(
|
|
json!({
|
|
"unstable": ["byonm"],
|
|
"nodeModulesDir": false,
|
|
}),
|
|
false,
|
|
NodeModulesDirMode::Manual,
|
|
),
|
|
(
|
|
json!({
|
|
"nodeModulesDir": true,
|
|
}),
|
|
true,
|
|
NodeModulesDirMode::Auto,
|
|
),
|
|
(
|
|
json!({
|
|
"nodeModulesDir": false,
|
|
}),
|
|
false,
|
|
NodeModulesDirMode::None,
|
|
),
|
|
];
|
|
|
|
for (config, previous, suggestion) in cases {
|
|
let workspace_dir = workspace_for_root_and_member(config, json!({}));
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![suggest(previous, suggestion)]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_pkg_only_fields_on_workspace_root() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"name": "@scope/name",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts"
|
|
}),
|
|
json!({}),
|
|
);
|
|
// this is fine because we can tell it's a package by it having name and exports fields
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_root_member_workspace_on_member() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({
|
|
"workspace": ["./other_dir"]
|
|
}),
|
|
);
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
// start at root for this test
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
vec![WorkspaceDiagnostic {
|
|
kind: WorkspaceDiagnosticKind::RootOnlyOption("workspace"),
|
|
config_url: Url::from_file_path(root_dir().join("member/deno.json"))
|
|
.unwrap(),
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_workspaces_property() {
|
|
run_single_json_diagnostics_test(
|
|
json!({
|
|
"workspaces": ["./member"]
|
|
}),
|
|
vec![WorkspaceDiagnosticKind::InvalidWorkspacesOption],
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_workspaces_missing_exports() {
|
|
run_single_json_diagnostics_test(
|
|
json!({
|
|
"name": "@scope/name",
|
|
}),
|
|
vec![WorkspaceDiagnosticKind::MissingExports],
|
|
);
|
|
}
|
|
|
|
fn run_single_json_diagnostics_test(
|
|
json: serde_json::Value,
|
|
kinds: Vec<WorkspaceDiagnosticKind>,
|
|
) {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(root_dir().join("deno.json"), json);
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
workspace_dir.workspace.diagnostics(),
|
|
kinds
|
|
.into_iter()
|
|
.map(|kind| {
|
|
WorkspaceDiagnostic {
|
|
kind,
|
|
config_url: Url::from_file_path(root_dir().join("deno.json"))
|
|
.unwrap(),
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_pkgs_same_name() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member1", "./member2"]
|
|
}),
|
|
);
|
|
let pkg = json!({
|
|
"name": "@scope/pkg",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
});
|
|
sys.fs_insert_json(
|
|
root_dir().join("member1").join("deno.json"),
|
|
pkg.clone(),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member2").join("deno.json"),
|
|
pkg.clone(),
|
|
);
|
|
let err = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap_err();
|
|
match err.into_kind() {
|
|
WorkspaceDiscoverErrorKind::ResolveMember(err) => match err.into_kind() {
|
|
ResolveWorkspaceMemberErrorKind::DuplicatePackageName {
|
|
name,
|
|
deno_json_url,
|
|
other_deno_json_url,
|
|
} => {
|
|
assert_eq!(name, "@scope/pkg");
|
|
assert_eq!(
|
|
deno_json_url,
|
|
Url::from_file_path(root_dir().join("member2").join("deno.json"))
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
other_deno_json_url,
|
|
Url::from_file_path(root_dir().join("member1").join("deno.json"))
|
|
.unwrap()
|
|
);
|
|
}
|
|
_ => unreachable!(),
|
|
},
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_packages_for_publish_non_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"name": "@scope/pkg",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("member"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/pkg"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_packages_for_publish_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./a", "./b", "./c", "./d"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("a/deno.json"),
|
|
json!({
|
|
"name": "@scope/a",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("b/deno.json"),
|
|
json!({
|
|
"name": "@scope/b",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("c/deno.json"),
|
|
// not a package
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("d/package.json"),
|
|
json!({
|
|
"name": "pkg",
|
|
"version": "1.0.0",
|
|
}),
|
|
);
|
|
// root
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/a", "@scope/b"]);
|
|
}
|
|
// member
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir().join("a"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/a"]);
|
|
}
|
|
// member, not a package
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir().join("c"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
assert!(jsr_pkgs.is_empty());
|
|
}
|
|
// package.json
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir().join("d"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
assert!(jsr_pkgs.is_empty());
|
|
|
|
// while we're here, test this
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.package_jsons()
|
|
.map(|p| p.dir_path().to_path_buf())
|
|
.collect::<Vec<_>>(),
|
|
vec![root_dir().join("d")]
|
|
);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.npm_packages()
|
|
.into_iter()
|
|
.map(|p| p.pkg_json.dir_path().to_path_buf())
|
|
.collect::<Vec<_>>(),
|
|
vec![root_dir().join("d")]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_packages_for_publish_root_is_package() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"name": "@scope/root",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({
|
|
"name": "@scope/pkg",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
// in a member
|
|
{
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("member"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/pkg"]);
|
|
}
|
|
// at the root
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
// Only returns the root package because it allows for publishing
|
|
// this individually. If someone wants the behaviour of publishing
|
|
// the entire workspace then they should move each package to a descendant
|
|
// directory.
|
|
assert_eq!(names, vec!["@scope/root"]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_packages_for_publish_root_not_package() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({
|
|
"name": "@scope/pkg",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
// the workspace is not a jsr package so publish the members
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/pkg"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_packages_for_publish_npm_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./a", "./b", "./c", "./d"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("a/package.json"), json!({}));
|
|
sys.fs_insert_json(
|
|
root_dir().join("a/deno.json"),
|
|
json!({
|
|
"name": "@scope/a",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("b/package.json"), json!({}));
|
|
sys.fs_insert_json(
|
|
root_dir().join("b/deno.json"),
|
|
json!({
|
|
"name": "@scope/b",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("c/package.json"), json!({}));
|
|
sys.fs_insert_json(
|
|
root_dir().join("c/deno.json"),
|
|
// not a package
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("d/package.json"),
|
|
json!({
|
|
"name": "pkg",
|
|
"version": "1.0.0",
|
|
}),
|
|
);
|
|
// root
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/a", "@scope/b"]);
|
|
}
|
|
// member
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir().join("a"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/a"]);
|
|
}
|
|
// member, not a package
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir().join("c"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
assert!(jsr_pkgs.is_empty());
|
|
}
|
|
// package.json
|
|
{
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir().join("d"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
assert!(jsr_pkgs.is_empty());
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.npm_packages()
|
|
.into_iter()
|
|
.map(|p| p.pkg_json.dir_path().to_path_buf())
|
|
.collect::<Vec<_>>(),
|
|
vec![root_dir().join("d")]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_auto_discovery_node_modules_dir() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(root_dir().join("deno.json"), json!({}));
|
|
sys.fs_insert_json(
|
|
root_dir().join("node_modules/package/package.json"),
|
|
json!({
|
|
"name": "@scope/pkg",
|
|
"version": "1.0.0"
|
|
}),
|
|
);
|
|
let workspace_dir = workspace_at_start_dir(
|
|
&sys,
|
|
&root_dir().join("node_modules/package/sub_dir"),
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 0);
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deno_workspace_globs() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./packages/*"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-a/deno.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-b/deno.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-c/deno.jsonc"),
|
|
json!({}),
|
|
);
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("packages"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deno_workspace_globs_with_package_json() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./packages/*", "./examples/*"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-a/deno.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-b/deno.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-c/deno.jsonc"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("examples/examples1/package.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("examples/examples2/package.json"),
|
|
json!({}),
|
|
);
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("packages"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 4);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deno_workspace_negations() {
|
|
for negation in ["!ignored/package-c", "!ignored/**"] {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": [
|
|
"**/*",
|
|
negation,
|
|
]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-a/deno.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-b/deno.jsonc"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("ignored/package-c/deno.jsonc"),
|
|
json!({}),
|
|
);
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_deno_workspace_member_no_config_file_error() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
// no deno.json in this folder, so should error
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir().join("package"));
|
|
assert_eq!(
|
|
err.to_string(),
|
|
normalize_err_text(
|
|
"Could not find config file for workspace member in '[ROOT_DIR_URL]/member/'."
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deno_workspace_member_deno_json_member_name() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member/deno.json"]
|
|
}),
|
|
);
|
|
// no deno.json in this folder and the name was deno.json so give an error
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir().join("package"));
|
|
assert_eq!(
|
|
err.to_string(),
|
|
normalize_err_text(concat!(
|
|
"Could not find config file for workspace member in '[ROOT_DIR_URL]/member/deno.json/'. ",
|
|
"Ensure you specify the directory and not the configuration file in the workspace member."
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deno_member_not_referenced_in_deno_workspace() {
|
|
fn assert_err(err: &WorkspaceDiscoverError, config_file_path: &Path) {
|
|
match err.as_kind() {
|
|
WorkspaceDiscoverErrorKind::ConfigNotWorkspaceMember {
|
|
workspace_url,
|
|
config_url,
|
|
} => {
|
|
assert_eq!(
|
|
workspace_url,
|
|
&url_from_directory_path(&root_dir()).unwrap()
|
|
);
|
|
assert_eq!(
|
|
config_url,
|
|
&Url::from_file_path(config_file_path).unwrap()
|
|
);
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
for file_name in ["deno.json", "deno.jsonc"] {
|
|
let config_file_path = root_dir().join("member-b").join(file_name);
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member-a"],
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member-a/deno.json"), json!({}));
|
|
sys.fs_insert_json(config_file_path.clone(), json!({}));
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir().join("member-b"));
|
|
assert_err(&err, &config_file_path);
|
|
|
|
// try for when the config file is specified as well
|
|
let err = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::ConfigFile(&config_file_path),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap_err();
|
|
assert_err(&err, &config_file_path);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_not_deno_workspace_member_non_natural_config_file_name() {
|
|
for file_name in ["other-name.json", "deno.jsonc"] {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member-a", "./member-b"],
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member-a/deno.json"), json!({}));
|
|
// this is the "natural" config file that would be discovered by
|
|
// workspace discovery and since the file name specified does not
|
|
// match it, the workspace is not discovered and an error does not
|
|
// occur
|
|
sys.fs_insert_json(root_dir().join("member-b/deno.json"), json!({}));
|
|
let config_file_path = root_dir().join("member-b").join(file_name);
|
|
sys.fs_insert_json(config_file_path.clone(), json!({}));
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::ConfigFile(&config_file_path),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.deno_jsons()
|
|
.map(|c| c.specifier.to_file_path().unwrap())
|
|
.collect::<Vec<_>>(),
|
|
vec![config_file_path]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_workspace_non_natural_config_file_name() {
|
|
let sys = InMemorySys::default();
|
|
let root_config_path = root_dir().join("deno-other.json");
|
|
sys.fs_insert_json(
|
|
root_config_path.clone(),
|
|
json!({
|
|
"workspace": ["./member-a"],
|
|
}),
|
|
);
|
|
let member_a_config = root_dir().join("member-a/deno.json");
|
|
sys.fs_insert_json(member_a_config.clone(), json!({}));
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::ConfigFile(&root_config_path),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.deno_jsons()
|
|
.map(|c| c.specifier.to_file_path().unwrap())
|
|
.collect::<Vec<_>>(),
|
|
vec![root_config_path, member_a_config]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_package_not_referenced_in_deno_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/deno.json"), json!({}));
|
|
sys.fs_insert_json(root_dir().join("package/package.json"), json!({}));
|
|
// npm package needs to be a member of the deno workspace
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir().join("package"));
|
|
assert_eq!(
|
|
err.to_string(),
|
|
normalize_err_text(
|
|
"Config file must be a member of the workspace.
|
|
Config: [ROOT_DIR_URL]/package/package.json
|
|
Workspace: [ROOT_DIR_URL]/"
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_workspaces_npm_package_referenced_in_package_json_workspace()
|
|
{
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./package"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/deno.json"), json!({}));
|
|
sys.fs_insert_json(root_dir().join("package/package.json"), json!({}));
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("package"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 2);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_package_json_and_deno_json_ok() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/deno.json"), json!({}));
|
|
sys.fs_insert_json(root_dir().join("member/package.json"), json!({}));
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("package"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 1);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_member_deno_json_error() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member"]
|
|
}),
|
|
);
|
|
// no package.json in this folder, so should error
|
|
sys.fs_insert_json(root_dir().join("member/deno.json"), json!({}));
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir().join("package"));
|
|
assert_eq!(
|
|
err.to_string(),
|
|
normalize_err_text(
|
|
"Could not find package.json for workspace member in '[ROOT_DIR_URL]/member/'."
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_member_no_config_file_error() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member"]
|
|
}),
|
|
);
|
|
// no package.json in this folder, so should error
|
|
let err = workspace_at_start_dir_err(&sys, &root_dir().join("package"));
|
|
assert_eq!(
|
|
err.to_string(),
|
|
normalize_err_text(
|
|
"Could not find package.json for workspace member in '[ROOT_DIR_URL]/member/'."
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_globs() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./packages/*"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-a/package.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-b/package.json"),
|
|
json!({}),
|
|
);
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("packages"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_ignores_vendor_folder() {
|
|
for (is_vendor, expected_count) in [(true, 3), (false, 4)] {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"vendor": is_vendor,
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./**/*"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-a/package.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-b/package.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("vendor/package-c/package.json"),
|
|
json!({}),
|
|
);
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(
|
|
workspace_dir.workspace.package_jsons().count(),
|
|
expected_count
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_negations() {
|
|
for negation in ["!ignored/package-c", "!ignored/**"] {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": [
|
|
"**/*",
|
|
negation,
|
|
]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-a/package.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("packages/package-b/package.json"),
|
|
json!({}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("ignored/package-c/package.json"),
|
|
json!({}),
|
|
);
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_self_reference_and_duplicate_references_ok() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": [
|
|
".",
|
|
"./member",
|
|
"./member",
|
|
"**/*"
|
|
]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/package.json"), json!({}));
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_start_deno_json_not_in_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./package"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({
|
|
"unstable": ["byonm"],
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("package/package.json"), json!({}));
|
|
// only resolves the member because it's not part of the workspace
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("member"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 1);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.root_dir_url()
|
|
.to_file_path()
|
|
.unwrap(),
|
|
root_dir().join("member")
|
|
);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 0);
|
|
assert!(workspace_dir.workspace.has_unstable("byonm"));
|
|
assert_eq!(
|
|
workspace_dir.workspace.resolve_lockfile_path().unwrap(),
|
|
Some(root_dir().join("member/deno.lock"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_start_deno_json_part_of_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({
|
|
"lock": false,
|
|
"unstable": ["byonm"],
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/package.json"), json!({}));
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("member"));
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.diagnostics()
|
|
.into_iter()
|
|
.map(|d| d.kind)
|
|
.collect::<Vec<_>>(),
|
|
vec![
|
|
WorkspaceDiagnosticKind::RootOnlyOption("lock"),
|
|
WorkspaceDiagnosticKind::RootOnlyOption("unstable")
|
|
]
|
|
);
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 1);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.root_dir_url()
|
|
.to_file_path()
|
|
.unwrap(),
|
|
root_dir()
|
|
);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 2);
|
|
assert!(!workspace_dir.workspace.has_unstable("byonm"));
|
|
assert_eq!(
|
|
workspace_dir.workspace.resolve_lockfile_path().unwrap(),
|
|
Some(root_dir().join("deno.lock"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_start_deno_json_part_of_workspace_sub_folder() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({
|
|
"unstable": ["byonm"],
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/package.json"), json!({}));
|
|
sys.fs_insert("member/sub/sub_folder/sub/file.ts", "");
|
|
let workspace_dir = workspace_at_start_dir(
|
|
&sys,
|
|
// note how we're starting in a sub folder of the member
|
|
&root_dir().join("member/sub/sub_folder/sub/"),
|
|
);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.diagnostics()
|
|
.into_iter()
|
|
.map(|d| d.kind)
|
|
.collect::<Vec<_>>(),
|
|
vec![WorkspaceDiagnosticKind::RootOnlyOption("unstable")]
|
|
);
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 1);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.root_dir_url()
|
|
.to_file_path()
|
|
.unwrap(),
|
|
root_dir()
|
|
);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 2);
|
|
assert!(!workspace_dir.workspace.has_unstable("byonm"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_start_deno_json_part_of_workspace_sub_folder_other_deno_json()
|
|
{
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member", "./member/sub"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/deno.json"),
|
|
json!({ "unstable": ["sloppy-imports"] }),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/package.json"), json!({}));
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/sub/deno.json"),
|
|
json!({ "unstable": ["byonm"] }),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/sub/package.json"), json!({}));
|
|
sys.fs_insert("member/sub/sub_folder/sub/file.ts", "");
|
|
let workspace_dir = workspace_at_start_dir(
|
|
&sys,
|
|
// note how we're starting in a sub folder of the member
|
|
&root_dir().join("member/sub/sub_folder/sub/"),
|
|
);
|
|
assert_eq!(workspace_dir.workspace.diagnostics().len(), 2); // for each unstable
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 2);
|
|
assert_eq!(
|
|
workspace_dir.workspace.root_dir_url.to_file_path().unwrap(),
|
|
root_dir()
|
|
);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 3);
|
|
assert!(!workspace_dir.workspace.has_unstable("sloppy-imports"));
|
|
assert!(!workspace_dir.workspace.has_unstable("byonm"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_start_package_json_not_in_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./package"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member/package.json"), json!({}));
|
|
sys.fs_insert_json(root_dir().join("package/package.json"), json!({}));
|
|
// only resolves the member because it's not part of the workspace
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("member"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 0);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.root_dir_url()
|
|
.to_file_path()
|
|
.unwrap(),
|
|
root_dir().join("member")
|
|
);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_multiple_dirs() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("workspace").join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("workspace").join("member/deno.json"),
|
|
json!({
|
|
"name": "@scope/pkg",
|
|
"version": "1.0.0",
|
|
"exports": "./main.ts",
|
|
}),
|
|
);
|
|
let workspace_dir = workspace_at_start_dirs(
|
|
&sys,
|
|
&[
|
|
root_dir().join("workspace/member"),
|
|
root_dir().join("other_dir"), // will be ignored because it's not in the workspace
|
|
],
|
|
)
|
|
.unwrap();
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), vec![]);
|
|
let jsr_pkgs = workspace_dir.jsr_packages_for_publish();
|
|
let names = jsr_pkgs.iter().map(|p| p.name.as_str()).collect::<Vec<_>>();
|
|
assert_eq!(names, vec!["@scope/pkg"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_ignore_pkg_json_between_member_and_root() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member/nested"]
|
|
}),
|
|
);
|
|
// will ignore this one
|
|
sys.fs_insert_json(root_dir().join("member/package.json"), json!({}));
|
|
sys
|
|
.fs_insert_json(root_dir().join("member/nested/package.json"), json!({}));
|
|
// only resolves the member because it's not part of the workspace
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("member/nested"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 0);
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.package_jsons()
|
|
.map(|p| p.path.clone())
|
|
.collect::<Vec<_>>(),
|
|
vec![
|
|
root_dir().join("package.json"),
|
|
root_dir().join("member/nested/package.json"),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_npm_workspace_ignore_deno_json_between_member_and_root() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({
|
|
"workspaces": ["./member/nested"]
|
|
}),
|
|
);
|
|
// will ignore this one
|
|
sys.fs_insert_json(root_dir().join("member/deno.json"), json!({}));
|
|
sys
|
|
.fs_insert_json(root_dir().join("member/nested/package.json"), json!({}));
|
|
// only resolves the member because it's not part of the workspace
|
|
let workspace_dir =
|
|
workspace_at_start_dir(&sys, &root_dir().join("member/nested"));
|
|
assert_eq!(workspace_dir.workspace.diagnostics(), Vec::new());
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 0);
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_multiple_dirs_outside_config() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("workspace/deno.json"),
|
|
json!({
|
|
"workspace": {
|
|
"members": ["./member"]
|
|
},
|
|
}),
|
|
);
|
|
sys
|
|
.fs_insert_json(root_dir().join("workspace/member/deno.json"), json!({}));
|
|
// this one will cause issues because it's not in the workspace
|
|
sys.fs_insert_json(root_dir().join("other_dir/deno.json"), json!({}));
|
|
let err = workspace_at_start_dirs(
|
|
&sys,
|
|
&[
|
|
root_dir().join("workspace/member"),
|
|
root_dir().join("other_dir"),
|
|
],
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(err.to_string(), normalize_err_text("Command resolved to multiple config files. Ensure all specified paths are within the same workspace.
|
|
First: [ROOT_DIR_URL]/workspace/deno.json
|
|
Second: [ROOT_DIR_URL]/other_dir/deno.json"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_multiple_dirs_outside_workspace() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("workspace/deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys
|
|
.fs_insert_json(root_dir().join("workspace/member/deno.json"), json!({}));
|
|
// this one will cause issues because it's not in the workspace
|
|
sys.fs_insert_json(
|
|
root_dir().join("other_dir/deno.json"),
|
|
json!({
|
|
"workspace": ["./member"]
|
|
}),
|
|
);
|
|
sys
|
|
.fs_insert_json(root_dir().join("other_dir/member/deno.json"), json!({}));
|
|
let err = workspace_at_start_dirs(
|
|
&sys,
|
|
&[
|
|
root_dir().join("workspace/member"),
|
|
root_dir().join("other_dir"),
|
|
],
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(err.to_string(), normalize_err_text("Command resolved to multiple config files. Ensure all specified paths are within the same workspace.
|
|
First: [ROOT_DIR_URL]/workspace/deno.json
|
|
Second: [ROOT_DIR_URL]/other_dir/deno.json"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_specified_config_file_same_dir_discoverable_config_file() {
|
|
let sys = InMemorySys::default();
|
|
// should not start discovering this deno.json because it
|
|
// should search for a workspace in the parent dir
|
|
sys.fs_insert_json(root_dir().join("sub_dir/deno.json"), json!({}));
|
|
let other_deno_json = root_dir().join("sub_dir/deno_other_name.json");
|
|
sys.fs_insert_json(&other_deno_json, json!({}));
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::ConfigFile(&other_deno_json),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.deno_jsons()
|
|
.map(|d| d.specifier.clone())
|
|
.collect::<Vec<_>>(),
|
|
vec![Url::from_file_path(&other_deno_json).unwrap()]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_workspace() {
|
|
let sys = InMemorySys::default();
|
|
let root_config_path = root_dir().join("deno.json");
|
|
sys.fs_insert_json(
|
|
root_config_path.clone(),
|
|
json!({
|
|
"workspace": ["./member-a"],
|
|
}),
|
|
);
|
|
let member_a_config = root_dir().join("member-a/deno.json");
|
|
sys.fs_insert_json(member_a_config.clone(), json!({}));
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::ConfigFile(&root_config_path),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.deno_jsons()
|
|
.map(|c| c.specifier.to_file_path().unwrap())
|
|
.collect::<Vec<_>>(),
|
|
vec![root_config_path, member_a_config]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_split_cli_args_by_deno_json_folder() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member-a", "./member-b"],
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member-a/deno.json"), json!({}));
|
|
sys.fs_insert_json(root_dir().join("member-b/deno.json"), json!({}));
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
// single member
|
|
{
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a"),
|
|
)])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-a")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
base: root_dir().join("member-a"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a")
|
|
)])),
|
|
exclude: Default::default(),
|
|
}
|
|
)])
|
|
);
|
|
}
|
|
// root and in single member
|
|
{
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(root_dir().join("member-a").join("sub")),
|
|
PathOrPattern::Path(root_dir().join("file")),
|
|
])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([
|
|
(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-a")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
base: root_dir().join("member-a/sub"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a").join("sub")
|
|
)])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
(
|
|
new_rc(url_from_directory_path(&root_dir()).unwrap()),
|
|
FilePatterns {
|
|
base: root_dir().join("file"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("file")
|
|
)])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
])
|
|
);
|
|
}
|
|
// multiple members (one with glob) and outside folder
|
|
{
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(root_dir().join("member-a")),
|
|
PathOrPattern::Pattern(
|
|
GlobPattern::from_relative(&root_dir().join("member-b"), "**/*")
|
|
.unwrap(),
|
|
),
|
|
PathOrPattern::Path(root_dir().join("other_dir")),
|
|
])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([
|
|
(
|
|
new_rc(url_from_directory_path(&root_dir()).unwrap()),
|
|
FilePatterns {
|
|
base: root_dir().join("other_dir"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("other_dir")
|
|
)])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-a")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
base: root_dir().join("member-a"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a")
|
|
)])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-b")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
base: root_dir().join("member-b"),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Pattern(
|
|
GlobPattern::from_relative(
|
|
&root_dir().join("member-b"),
|
|
"**/*"
|
|
)
|
|
.unwrap(),
|
|
)
|
|
])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
])
|
|
);
|
|
}
|
|
// glob at root dir
|
|
{
|
|
let root_glob = PathOrPattern::Pattern(
|
|
GlobPattern::from_relative(&root_dir(), "**/*").unwrap(),
|
|
);
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![root_glob.clone()])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([
|
|
(
|
|
new_rc(url_from_directory_path(&root_dir()).unwrap()),
|
|
FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![root_glob.clone()])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-a")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
base: root_dir().join("member-a"),
|
|
include: Some(PathOrPatternSet::new(vec![root_glob.clone()])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-b")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
base: root_dir().join("member-b"),
|
|
include: Some(PathOrPatternSet::new(vec![root_glob])),
|
|
exclude: Default::default(),
|
|
}
|
|
),
|
|
])
|
|
);
|
|
}
|
|
// single path in descendant of member
|
|
{
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a/sub-dir/descendant/further"),
|
|
)])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-a")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
base: root_dir().join("member-a/sub-dir/descendant/further"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a/sub-dir/descendant/further"),
|
|
)])),
|
|
exclude: Default::default(),
|
|
}
|
|
),])
|
|
);
|
|
}
|
|
// path in descendant of member then second path that goes to a parent folder
|
|
{
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(
|
|
root_dir().join("member-a/sub-dir/descendant/further"),
|
|
),
|
|
PathOrPattern::Path(root_dir().join("member-a/sub-dir/other")),
|
|
])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([(
|
|
new_rc(
|
|
url_from_directory_path(&root_dir().join("member-a")).unwrap()
|
|
),
|
|
FilePatterns {
|
|
// should use common base here
|
|
base: root_dir().join("member-a/sub-dir"),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(
|
|
root_dir().join("member-a/sub-dir/descendant/further"),
|
|
),
|
|
PathOrPattern::Path(root_dir().join("member-a/sub-dir/other"),)
|
|
])),
|
|
exclude: Default::default(),
|
|
}
|
|
)])
|
|
);
|
|
}
|
|
// path outside the root directory
|
|
{
|
|
let dir_outside =
|
|
normalize_path(root_dir().join("../dir_outside").into());
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
dir_outside.to_path_buf(),
|
|
)])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([(
|
|
new_rc(url_from_directory_path(&root_dir()).unwrap()),
|
|
FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
dir_outside.to_path_buf(),
|
|
),])),
|
|
exclude: Default::default(),
|
|
}
|
|
)])
|
|
);
|
|
}
|
|
// multiple paths outside the root directory
|
|
{
|
|
let dir_outside_1 =
|
|
normalize_path(root_dir().join("../dir_outside_1").into());
|
|
let dir_outside_2 =
|
|
normalize_path(root_dir().join("../dir_outside_2").into());
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(dir_outside_1.to_path_buf()),
|
|
PathOrPattern::Path(dir_outside_2.to_path_buf()),
|
|
])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([(
|
|
new_rc(url_from_directory_path(&root_dir()).unwrap()),
|
|
FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(dir_outside_1.to_path_buf()),
|
|
PathOrPattern::Path(dir_outside_2.to_path_buf()),
|
|
])),
|
|
exclude: Default::default(),
|
|
}
|
|
)])
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_split_cli_args_by_deno_json_folder_no_config() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert(root_dir().join("path"), ""); // create the root directory
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
// two paths, looped to ensure that the order is maintained on
|
|
// the output and not sorted
|
|
let path1 = normalize_path(root_dir().join("./path-longer").into());
|
|
let path2 = normalize_path(root_dir().join("./path").into());
|
|
for (path1, path2) in [(&path1, &path2), (&path2, &path1)] {
|
|
let split = workspace_dir.workspace.split_cli_args_by_deno_json_folder(
|
|
&FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(path1.to_path_buf()),
|
|
PathOrPattern::Path(path2.to_path_buf()),
|
|
])),
|
|
exclude: Default::default(),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
split,
|
|
IndexMap::from([(
|
|
new_rc(url_from_directory_path(&root_dir()).unwrap()),
|
|
FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(path1.to_path_buf()),
|
|
PathOrPattern::Path(path2.to_path_buf()),
|
|
])),
|
|
exclude: Default::default(),
|
|
}
|
|
)])
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_config_for_members_include_root_and_sub_member() {
|
|
fn run_test(
|
|
config_key: &str,
|
|
workspace_to_file_patterns: impl Fn(&WorkspaceDirectory) -> Vec<FilePatterns>,
|
|
) {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member-a", "./member-b", "member-c"],
|
|
config_key: {
|
|
"include": ["./file.ts", "./member-c/file.ts"]
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member-a/deno.json"),
|
|
json!({
|
|
config_key: {
|
|
"include": ["./member-a-file.ts"]
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member-b/deno.json"), json!({}));
|
|
sys.fs_insert_json(root_dir().join("member-c/deno.json"), json!({}));
|
|
let workspace = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(
|
|
workspace_to_file_patterns(&workspace),
|
|
vec![
|
|
FilePatterns {
|
|
base: root_dir(),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("file.ts")
|
|
)])),
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(root_dir().join("member-a")),
|
|
PathOrPattern::Path(root_dir().join("member-b")),
|
|
PathOrPattern::Path(root_dir().join("member-c")),
|
|
])
|
|
},
|
|
FilePatterns {
|
|
base: root_dir().join("member-a"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a").join("member-a-file.ts")
|
|
)])),
|
|
exclude: Default::default(),
|
|
},
|
|
FilePatterns {
|
|
base: root_dir().join("member-b"),
|
|
include: None,
|
|
exclude: Default::default(),
|
|
},
|
|
FilePatterns {
|
|
base: root_dir().join("member-c"),
|
|
include: Some(PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-c").join("file.ts")
|
|
)])),
|
|
exclude: Default::default(),
|
|
}
|
|
]
|
|
);
|
|
}
|
|
|
|
run_test("bench", |workspace_dir| {
|
|
let config_for_members = workspace_dir
|
|
.workspace
|
|
.resolve_bench_config_for_members(&FilePatterns::new_with_base(
|
|
root_dir(),
|
|
))
|
|
.unwrap();
|
|
config_for_members
|
|
.into_iter()
|
|
.map(|(_ctx, config)| config.files)
|
|
.collect::<Vec<_>>()
|
|
});
|
|
|
|
run_test("fmt", |workspace_dir| {
|
|
let config_for_members = workspace_dir
|
|
.workspace
|
|
.resolve_fmt_config_for_members(
|
|
&FilePatterns::new_with_base(root_dir()),
|
|
)
|
|
.unwrap();
|
|
config_for_members
|
|
.into_iter()
|
|
.map(|(_ctx, config)| config.files)
|
|
.collect::<Vec<_>>()
|
|
});
|
|
|
|
run_test("lint", |workspace_dir| {
|
|
let config_for_members = workspace_dir
|
|
.workspace
|
|
.resolve_lint_config_for_members(&FilePatterns::new_with_base(
|
|
root_dir(),
|
|
))
|
|
.unwrap();
|
|
config_for_members
|
|
.into_iter()
|
|
.map(|(_ctx, config)| config.files)
|
|
.collect::<Vec<_>>()
|
|
});
|
|
|
|
run_test("test", |workspace_dir| {
|
|
let config_for_members = workspace_dir
|
|
.workspace
|
|
.resolve_test_config_for_members(&FilePatterns::new_with_base(
|
|
root_dir(),
|
|
))
|
|
.unwrap();
|
|
config_for_members
|
|
.into_iter()
|
|
.map(|(_ctx, config)| config.files)
|
|
.collect::<Vec<_>>()
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_config_for_members_excluded_member() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member-a", "./member-b"],
|
|
"lint": {
|
|
"exclude": ["./member-a"]
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(root_dir().join("member-a/deno.json"), json!({}));
|
|
sys.fs_insert_json(root_dir().join("member-b/deno.json"), json!({}));
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
let config_for_members = workspace_dir
|
|
.workspace
|
|
.resolve_lint_config_for_members(&FilePatterns::new_with_base(root_dir()))
|
|
.unwrap();
|
|
let file_patterns = config_for_members
|
|
.into_iter()
|
|
.map(|(_ctx, config)| config.files)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
file_patterns,
|
|
vec![
|
|
FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(root_dir().join("member-a")),
|
|
// It will be in here twice because it's excluded from being
|
|
// traversed for this set of FilePatterns and also it's excluded
|
|
// in the "exclude". This is not a big deal because it's an edge
|
|
// case and the end behaviour is the same. It's probably not worth
|
|
// the complexity and perf to ensure only unique items are in here
|
|
PathOrPattern::Path(root_dir().join("member-a")),
|
|
PathOrPattern::Path(root_dir().join("member-b")),
|
|
])
|
|
},
|
|
// This item is effectively a no-op as it excludes itself.
|
|
// It would be nice to have this not even included as a member,
|
|
// but doing that in a maintainable way would require a bit of
|
|
// refactoring to get resolve_config_for_members to understand
|
|
// that configs return FilePatterns.
|
|
FilePatterns {
|
|
base: root_dir().join("member-a"),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![PathOrPattern::Path(
|
|
root_dir().join("member-a")
|
|
),]),
|
|
},
|
|
FilePatterns {
|
|
base: root_dir().join("member-b"),
|
|
include: None,
|
|
exclude: Default::default(),
|
|
},
|
|
]
|
|
);
|
|
|
|
// ensure the second file patterns is a no-op
|
|
sys.fs_insert(root_dir().join("member-a/file.ts"), "");
|
|
sys.fs_insert(root_dir().join("member-a/sub-dir/file.ts"), "");
|
|
let files = FileCollector::new(|_| true)
|
|
.collect_file_patterns(&sys, &file_patterns[1]);
|
|
assert!(files.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_config_for_members_excluded_member_unexcluded_sub_dir() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./member-a"],
|
|
"lint": {
|
|
"exclude": ["./member-a"]
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member-a/deno.json"),
|
|
json!({
|
|
"lint": {
|
|
// unexclude this sub dir so it's linted
|
|
"exclude": ["!./sub-dir"]
|
|
}
|
|
}),
|
|
);
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
let config_for_members = workspace_dir
|
|
.workspace
|
|
.resolve_lint_config_for_members(&FilePatterns::new_with_base(root_dir()))
|
|
.unwrap();
|
|
let file_patterns = config_for_members
|
|
.into_iter()
|
|
.map(|(_ctx, config)| config.files)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
file_patterns,
|
|
vec![
|
|
FilePatterns {
|
|
base: root_dir(),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![
|
|
PathOrPattern::Path(root_dir().join("member-a")),
|
|
// see note in previous test about this being here twice
|
|
PathOrPattern::Path(root_dir().join("member-a")),
|
|
])
|
|
},
|
|
FilePatterns {
|
|
base: root_dir().join("member-a"),
|
|
include: None,
|
|
exclude: PathOrPatternSet::new(vec![
|
|
// self will be excluded, but then sub dir will be unexcluded
|
|
PathOrPattern::Path(root_dir().join("member-a")),
|
|
PathOrPattern::NegatedPath(
|
|
root_dir().join("member-a").join("sub-dir")
|
|
),
|
|
]),
|
|
},
|
|
]
|
|
);
|
|
sys.fs_insert(root_dir().join("member-a/file.ts"), "");
|
|
sys.fs_insert(root_dir().join("member-a/sub-dir/file.ts"), "");
|
|
let files = FileCollector::new(|_| true)
|
|
.collect_file_patterns(&sys, &file_patterns[1]);
|
|
// should only have member-a/sub-dir/file.ts and not member-a/file.ts
|
|
assert_eq!(files, vec![root_dir().join("member-a/sub-dir/file.ts")]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_lock_path() {
|
|
let workspace_dir = workspace_for_root_and_member(
|
|
json!({
|
|
"lock": "other.lock",
|
|
}),
|
|
json!({}),
|
|
);
|
|
assert_eq!(
|
|
workspace_dir.workspace.resolve_lockfile_path().unwrap(),
|
|
Some(root_dir().join("other.lock"))
|
|
);
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct DenoJsonMemCache(RefCell<HashMap<PathBuf, ConfigFileRc>>);
|
|
|
|
impl DenoJsonCache for DenoJsonMemCache {
|
|
fn get(&self, path: &Path) -> Option<ConfigFileRc> {
|
|
self.0.borrow().get(path).cloned()
|
|
}
|
|
|
|
fn set(&self, path: PathBuf, deno_json: ConfigFileRc) {
|
|
self.0.borrow_mut().insert(path, deno_json);
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PkgJsonMemCache(RefCell<HashMap<PathBuf, PackageJsonRc>>);
|
|
|
|
impl deno_package_json::PackageJsonCache for PkgJsonMemCache {
|
|
fn get(&self, path: &Path) -> Option<PackageJsonRc> {
|
|
self.0.borrow().get(path).cloned()
|
|
}
|
|
|
|
fn set(&self, path: PathBuf, value: PackageJsonRc) {
|
|
self.0.borrow_mut().insert(path, value);
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct WorkspaceMemCache(RefCell<HashMap<PathBuf, WorkspaceRc>>);
|
|
|
|
impl WorkspaceCache for WorkspaceMemCache {
|
|
fn get(&self, dir_path: &Path) -> Option<WorkspaceRc> {
|
|
self.0.borrow().get(dir_path).cloned()
|
|
}
|
|
|
|
fn set(&self, dir_path: PathBuf, workspace: WorkspaceRc) {
|
|
self.0.borrow_mut().insert(dir_path, workspace);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_discovery_deno_json_cache() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({ "nodeModulesDir": true }),
|
|
);
|
|
let cache = DenoJsonMemCache::default();
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
deno_json_cache: Some(&cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(cache.0.borrow().len(), 1); // writes to the cache
|
|
assert_eq!(
|
|
workspace_dir.workspace.node_modules_dir().unwrap(),
|
|
Some(NodeModulesDirMode::Auto)
|
|
);
|
|
let new_config_file = ConfigFile::new(
|
|
r#"{ "nodeModulesDir": false }"#,
|
|
Url::from_file_path(root_dir().join("deno.json")).unwrap(),
|
|
)
|
|
.unwrap();
|
|
cache
|
|
.0
|
|
.borrow_mut()
|
|
.insert(root_dir().join("deno.json"), new_rc(new_config_file));
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
deno_json_cache: Some(&cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
workspace_dir.workspace.node_modules_dir().unwrap(),
|
|
Some(NodeModulesDirMode::None) // reads from the cache
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_discovery_pkg_json_cache() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({ "name": "member" }),
|
|
);
|
|
let cache = PkgJsonMemCache::default();
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
pkg_json_cache: Some(&cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(cache.0.borrow().len(), 1); // writes to the cache
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 1);
|
|
let new_pkg_json = PackageJson::load_from_string(
|
|
root_dir().join("package.json"),
|
|
r#"{ "name": "cached-name" }"#,
|
|
)
|
|
.unwrap();
|
|
cache
|
|
.0
|
|
.borrow_mut()
|
|
.insert(root_dir().join("package.json"), new_rc(new_pkg_json));
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
pkg_json_cache: Some(&cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
// reads from the cache
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.package_jsons()
|
|
.map(|p| p.name.as_deref().unwrap())
|
|
.collect::<Vec<_>>(),
|
|
vec!["cached-name"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_discovery_workspace_cache() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/package-a/package.json"),
|
|
json!({
|
|
"name": "member-a"
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/package-b/deno.json"),
|
|
json!({
|
|
"name": "member-b"
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["member/package-a", "member/package-b"]
|
|
}),
|
|
);
|
|
let deno_json_cache = DenoJsonMemCache::default();
|
|
let pkg_json_cache = PkgJsonMemCache::default();
|
|
let workspace_cache = WorkspaceMemCache::default();
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
deno_json_cache: Some(&deno_json_cache),
|
|
pkg_json_cache: Some(&pkg_json_cache),
|
|
workspace_cache: Some(&workspace_cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 1);
|
|
// writes to the caches
|
|
assert_eq!(pkg_json_cache.0.borrow().len(), 1);
|
|
assert_eq!(deno_json_cache.0.borrow().len(), 2);
|
|
assert_eq!(workspace_cache.0.borrow().len(), 1);
|
|
// now delete from the deno json and pkg json caches
|
|
deno_json_cache.0.borrow_mut().clear();
|
|
pkg_json_cache.0.borrow_mut().clear();
|
|
// should load and not write to the caches
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[root_dir()]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
deno_json_cache: Some(&deno_json_cache),
|
|
pkg_json_cache: Some(&pkg_json_cache),
|
|
workspace_cache: Some(&workspace_cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 1);
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 2);
|
|
// it wouldn't have written to these because it just
|
|
// loads from the workspace cache
|
|
assert_eq!(pkg_json_cache.0.borrow().len(), 0);
|
|
assert_eq!(deno_json_cache.0.borrow().len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn deno_workspace_discovery_workspace_cache() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/package-a/deno.json"),
|
|
json!({ "name": "member-a" }),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/package-b/deno.json"),
|
|
json!({ "name": "member-b" }),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({ "workspace": ["member/package-a", "member/package-b"] }),
|
|
);
|
|
let deno_json_cache = DenoJsonMemCache::default();
|
|
let pkg_json_cache = PkgJsonMemCache::default();
|
|
let workspace_cache = WorkspaceMemCache::default();
|
|
for start_dir in [
|
|
root_dir(),
|
|
root_dir().join("member/package-a"),
|
|
root_dir().join("member/package-b"),
|
|
] {
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[start_dir]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
deno_json_cache: Some(&deno_json_cache),
|
|
pkg_json_cache: Some(&pkg_json_cache),
|
|
workspace_cache: Some(&workspace_cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(workspace_dir.workspace.deno_jsons().count(), 3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn npm_workspace_discovery_workspace_cache() {
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/package-a/package.json"),
|
|
json!({ "name": "member-a" }),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("member/package-b/package.json"),
|
|
json!({ "name": "member-b" }),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("package.json"),
|
|
json!({ "workspaces": ["member/*"] }),
|
|
);
|
|
let deno_json_cache = DenoJsonMemCache::default();
|
|
let pkg_json_cache = PkgJsonMemCache::default();
|
|
let workspace_cache = WorkspaceMemCache::default();
|
|
for start_dir in [
|
|
root_dir(),
|
|
root_dir().join("member/package-a"),
|
|
root_dir().join("member/package-b"),
|
|
] {
|
|
let workspace_dir = WorkspaceDirectory::discover(
|
|
&sys,
|
|
WorkspaceDiscoverStart::Paths(&[start_dir]),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
deno_json_cache: Some(&deno_json_cache),
|
|
pkg_json_cache: Some(&pkg_json_cache),
|
|
workspace_cache: Some(&workspace_cache),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(workspace_dir.workspace.package_jsons().count(), 3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_folder_sorted_dependencies() {
|
|
#[track_caller]
|
|
fn assert_order(sys: InMemorySys, expected: Vec<PathBuf>) {
|
|
let workspace_dir = workspace_at_start_dir(&sys, &root_dir());
|
|
assert_eq!(
|
|
workspace_dir
|
|
.workspace
|
|
.config_folders_sorted_by_dependencies()
|
|
.keys()
|
|
.map(|k| k.to_file_path().unwrap())
|
|
.collect::<Vec<_>>(),
|
|
expected,
|
|
);
|
|
}
|
|
|
|
{
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./a", "./b", "./c"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("a/package.json"),
|
|
json!({
|
|
"dependencies": {
|
|
"c": "*"
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("b/package.json"),
|
|
json!({
|
|
"name": "b",
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("c/package.json"),
|
|
json!({
|
|
"name": "c",
|
|
"dependencies": {
|
|
"b": "workspace:~"
|
|
}
|
|
}),
|
|
);
|
|
assert_order(
|
|
sys,
|
|
vec![
|
|
root_dir(),
|
|
root_dir().join("b"),
|
|
root_dir().join("c"),
|
|
root_dir().join("a"),
|
|
],
|
|
);
|
|
}
|
|
|
|
// circular
|
|
{
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./a", "./b", "./c"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("a/package.json"),
|
|
json!({
|
|
"dependencies": {
|
|
"b": "*"
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("b/package.json"),
|
|
json!({
|
|
"name": "b",
|
|
"dependencies": {
|
|
"c": "*"
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("c/package.json"),
|
|
json!({
|
|
"name": "c",
|
|
"dependencies": {
|
|
"a": "*"
|
|
}
|
|
}),
|
|
);
|
|
assert_order(
|
|
sys,
|
|
vec![
|
|
root_dir(),
|
|
root_dir().join("c"),
|
|
root_dir().join("b"),
|
|
root_dir().join("a"),
|
|
],
|
|
);
|
|
}
|
|
|
|
// file specifier
|
|
{
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(
|
|
root_dir().join("deno.json"),
|
|
json!({
|
|
"workspace": ["./a", "./b", "./c"]
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("a/package.json"),
|
|
json!({
|
|
"dependencies": {
|
|
"b": "file:../b"
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("b/package.json"),
|
|
json!({
|
|
"name": "b",
|
|
"dependencies": {
|
|
"c": "file:../c/"
|
|
}
|
|
}),
|
|
);
|
|
sys.fs_insert_json(
|
|
root_dir().join("c/package.json"),
|
|
json!({
|
|
"name": "c"
|
|
}),
|
|
);
|
|
assert_order(
|
|
sys,
|
|
vec![
|
|
root_dir(),
|
|
root_dir().join("c"),
|
|
root_dir().join("b"),
|
|
root_dir().join("a"),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
fn workspace_for_root_and_member(
|
|
root: serde_json::Value,
|
|
member: serde_json::Value,
|
|
) -> WorkspaceDirectoryRc {
|
|
workspace_for_root_and_member_with_fs(root, member, |_| {})
|
|
}
|
|
|
|
fn workspace_for_root_and_member_with_fs(
|
|
root: serde_json::Value,
|
|
member: serde_json::Value,
|
|
with_sys: impl FnOnce(&InMemorySys),
|
|
) -> WorkspaceDirectoryRc {
|
|
let sys = in_memory_fs_for_root_and_member(root, member);
|
|
with_sys(&sys);
|
|
// start in the member
|
|
workspace_at_start_dir(&sys, &root_dir().join("member"))
|
|
}
|
|
|
|
fn in_memory_fs_for_root_and_member(
|
|
mut root: serde_json::Value,
|
|
member: serde_json::Value,
|
|
) -> InMemorySys {
|
|
root
|
|
.as_object_mut()
|
|
.unwrap()
|
|
.insert("workspace".to_string(), json!(["./member"]));
|
|
let sys = InMemorySys::default();
|
|
sys.fs_insert_json(root_dir().join("deno.json"), root);
|
|
sys.fs_insert_json(root_dir().join("member/deno.json"), member);
|
|
sys
|
|
}
|
|
|
|
fn workspace_at_start_dir(
|
|
sys: &InMemorySys,
|
|
start_dir: &Path,
|
|
) -> WorkspaceDirectoryRc {
|
|
workspace_at_start_dir_result(sys, start_dir).unwrap()
|
|
}
|
|
|
|
fn workspace_at_start_dir_err(
|
|
sys: &InMemorySys,
|
|
start_dir: &Path,
|
|
) -> WorkspaceDiscoverError {
|
|
workspace_at_start_dir_result(sys, start_dir).unwrap_err()
|
|
}
|
|
|
|
fn workspace_at_start_dir_result(
|
|
sys: &InMemorySys,
|
|
start_dir: &Path,
|
|
) -> Result<WorkspaceDirectoryRc, WorkspaceDiscoverError> {
|
|
workspace_at_start_dirs(sys, &[start_dir.to_path_buf()])
|
|
}
|
|
|
|
fn workspace_at_start_dirs(
|
|
sys: &InMemorySys,
|
|
start_dirs: &[PathBuf],
|
|
) -> Result<WorkspaceDirectoryRc, WorkspaceDiscoverError> {
|
|
WorkspaceDirectory::discover(
|
|
sys,
|
|
WorkspaceDiscoverStart::Paths(start_dirs),
|
|
&WorkspaceDiscoverOptions {
|
|
discover_pkg_json: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
}
|
|
|
|
fn normalize_err_text(text: &str) -> String {
|
|
text.replace(
|
|
"[ROOT_DIR_URL]",
|
|
url_from_directory_path(&root_dir())
|
|
.unwrap()
|
|
.to_string()
|
|
.trim_end_matches('/'),
|
|
)
|
|
}
|
|
}
|