1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
use std::{collections::HashSet, path::Path};
use crate::{Language, Package, update_type::UpdateType};
use anyhow::Result;
use async_trait::async_trait;
/// Interface for monorepo workspace roots.
///
/// Extends Package behavior with workspace-specific operations like updating workspace
/// dependencies. Implemented by language-specific workspace types.
#[async_trait]
pub trait Workspace: std::fmt::Debug + Send + Sync {
fn name(&self) -> Option<&str>;
fn path(&self) -> &Path;
fn relative_path(&self) -> &Path;
fn version(&self) -> Option<&str>;
/// # Errors
/// Returns error if the version update operation fails.
async fn update_version(&mut self, update_type: UpdateType) -> Result<()>;
fn language(&self) -> Language;
fn dependencies(&self) -> &HashSet<String>;
fn add_dependency(&mut self, dependency: &str);
fn is_changed(&self) -> bool;
fn set_changed(&mut self, changed: bool);
/// Set the workspace name (used for fallback when name is not found in manifest).
/// Implementors typically get this via `impl_basic_accessors!()`.
fn set_name(&mut self, name: String);
/// Get the default publish command for this workspace type
fn default_publish_command(&self) -> String;
/// Get the default dry-run publish command for this workspace type.
///
/// Returns `None` for ecosystems whose default publish tool does not
/// support a built-in dry-run mode. Users may still provide an override
/// via `config.publish_dry_run`.
fn default_dry_run_publish_command(&self) -> Option<String>;
crate::impl_shared_project_defaults!();
crate::impl_publish_flows!(crate::publish::WORKSPACE_DIR_NOT_FOUND);
crate::impl_publish_command_resolvers!();
/// Updates workspace-level dependency versions after package versions are bumped.
///
/// This is an intentional no-op in the default implementation. Only `RustWorkspace`
/// overrides this method to sync `[workspace.dependencies]` path-dependency versions
/// with their corresponding package versions.
///
/// # Errors
///
/// Returns an error only if a language override's dependency rewrite fails.
///
/// Boxed-future shape rather than a defaulted `async fn`, for the reason
/// spelled out on [`ProjectFinder::should_visit_manifest`](crate::ProjectFinder::should_visit_manifest):
/// `#[async_trait]`'s desugaring of a *defaulted* body is not attributed by
/// `llvm-cov`, so the no-op would read as unexecuted forever.
fn update_workspace_dependencies<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
_packages: &'life1 [&'life2 dyn Package],
) -> ::core::pin::Pin<
::std::boxed::Box<
dyn ::core::future::Future<Output = Result<()>> + ::core::marker::Send + 'async_trait,
>,
>
where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: ::core::marker::Sync + 'async_trait,
{
::std::boxed::Box::pin(async move { Ok(()) })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
use crate::test_support::MockWorkspace;
use std::collections::BTreeMap;
// The eighteen tests pinning the shared trait defaults and the shared
// `UnsupportedDryRunProject` fixture are generated from one surface shared
// with `package.rs`; only the `Workspace`-only defaults below stay
// hand-written here.
crate::test_support::shared_project_default_tests!(
mock: MockWorkspace,
trait_name: Workspace,
kind: "workspace",
dir_not_found: "Workspace directory not found",
publishable_test: test_workspace_is_publishable_by_default,
);
#[test]
fn test_get_dry_run_publish_command_falls_back_to_workspace_default() {
let workspace =
MockWorkspace::with_paths(Some("test"), "/project/package.json", "package.json")
.with_language(Language::Node);
let config = Config::default();
// With no override, the trait method returns the workspace's own
// `default_dry_run_publish_command()` (here, the MockWorkspace stub).
assert_eq!(
workspace.get_dry_run_publish_command(&config).as_deref(),
Some("echo publish --dry-run")
);
}
#[test]
fn test_get_dry_run_publish_command_override_by_path() {
let workspace = MockWorkspace::with_paths(
Some("test"),
"/project/package.json",
"packages/core/package.json",
);
let mut publish_dry_run = BTreeMap::new();
publish_dry_run.insert(
"packages/core/package.json".to_string(),
"custom dry".to_string(),
);
let config = Config {
publish_dry_run,
..Default::default()
};
// Per-project override wins over the workspace's own default.
assert_eq!(
workspace.get_dry_run_publish_command(&config).as_deref(),
Some("custom dry")
);
}
#[test]
fn test_get_dry_run_publish_command_override_by_language() {
let workspace =
MockWorkspace::with_paths(Some("test"), "/project/package.json", "package.json")
.with_language(Language::Node);
let mut publish_dry_run = BTreeMap::new();
publish_dry_run.insert(
"node".to_string(),
"npm publish --dry-run --tag next".to_string(),
);
let config = Config {
publish_dry_run,
..Default::default()
};
// Per-language override wins over the workspace's own default.
assert_eq!(
workspace.get_dry_run_publish_command(&config).as_deref(),
Some("npm publish --dry-run --tag next")
);
}
#[tokio::test]
async fn test_update_workspace_dependencies_default() {
let workspace =
MockWorkspace::with_paths(Some("test"), "/project/package.json", "package.json");
let packages: Vec<&dyn Package> = vec![];
let result = workspace.update_workspace_dependencies(&packages).await;
assert!(result.is_ok());
}
}