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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//! What: Plan patch bumps for publishable dependents of packages whose version differs from
//! the base, with the exact manifest bytes before and after each bump.
//! Why: This is the one planner behind both entry points: the commit-time policy and the
//! release workflow's ripple, which will call the native wrapper's direct fix. It is
//! the incumbent's `planWorkspaceBumps` (`dependent-bump-workflow.ts:280-388`) over the
//! content seam: it starts no Git process and reads no file.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const plan = await planWorkspaceBumps(reader);
//! ```
/// What: `use` brings names from sibling files into this file.
/// Why: Each step of the plan has its own module.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { readManifestStates, bumpedNames } from './dependent_version_states.ts';
/// ```
use super::dependent_version_bundled::bundled_development_edges;
/// The content seam and its failures.
use super::dependent_version_content::{PlanError, TrackedMode, TrackedPath, WorkspaceContent};
/// The dependency walk and the bump plan.
use super::dependent_version_graph::{PlannedBump, WorkspaceNode, plan_dependent_bumps};
/// Strict decoding of the registry configuration.
use super::dependent_version_manifest::strict_text;
/// The registry configuration's path.
use super::dependent_version_paths::PNPR_CONFIG_PATH;
/// The publish set from the registry configuration.
use super::dependent_version_publishable::read_publishable_names;
/// A dependent whose version cannot be bumped automatically.
use super::dependent_version_release::UnsupportedVersion;
/// Manifest states, raised names and duplicate names.
use super::dependent_version_states::{
ManifestState, bumped_names, duplicate_name, read_manifest_states,
};
/// The byte-preserving version rewrite.
use super::dependent_version_text::replace_manifest_version;
/// What: One dependent bump with the manifest change that performs it. `Vec<u8>` holds
/// exact file bytes.
/// Why: The policy turns it into a full-content patch of a tracked file; the release path
/// writes `replacement` after checking the file still holds `original`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ManifestBump = PlannedBump & { path: string; text: string; replacement: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ManifestBump {
/// The dependent, its directory and the version change.
pub planned: PlannedBump,
/// The manifest path.
pub path: Vec<u8>,
/// Its mode in the candidate state.
pub mode: TrackedMode,
/// Its exact current bytes.
pub original: Vec<u8>,
/// The same bytes with only the version value changed; a leading byte-order mark is
/// kept.
pub replacement: Vec<u8>,
}
/// What: A completed plan.
/// Why: The finding message names the raised packages; the bumps are sorted by name.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type WorkspaceBumpPlan = { bumpedNames: string[]; bumps: ManifestBump[] };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WorkspaceBumpPlan {
/// Packages whose version differs from the base, in manifest order.
pub bumped_names: Vec<String>,
/// Dependent bumps, sorted by name.
pub bumps: Vec<ManifestBump>,
}
/// What: How planning ended when nothing failed.
/// Why: A dependent that needs a bump but holds no plain release stops the plan; the
/// policy reports it for a bump by hand, the release path fails.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type PlanOutcome = { kind: 'planned'; plan: WorkspaceBumpPlan } | { kind: 'unsupported'; error: UnsupportedVersionError };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PlanOutcome {
/// Every needed bump was planned; the list may be empty.
Planned(
/// The plan.
WorkspaceBumpPlan,
),
/// The first dependent, in manifest order, whose version cannot be bumped.
Unsupported(
/// The dependent and its version.
UnsupportedVersion,
),
}
/// What: The publishable names from the registry configuration, or nothing when it is not
/// tracked.
/// Why: Without the configuration nothing is publishable, so nothing is bumped.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const [configText] = await reader.pnprConfigText();
/// ```
fn publishable_names(
content: &mut dyn WorkspaceContent,
tracked: &[TrackedPath],
) -> Result<Option<Vec<String>>, PlanError> {
let Some(config) = tracked
.iter()
.find(|entry: &&TrackedPath| return entry.path == PNPR_CONFIG_PATH)
else {
return Ok(None);
};
let bytes: Vec<u8> = content.candidate_bytes(&config.path)?;
return Ok(Some(read_publishable_names(strict_text(
&config.path,
&bytes,
)?)));
}
/// What: The manifest change for one planned bump.
/// Why: The edit rewrites the parsed text; the original bytes before it (a byte-order
/// mark) are kept.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const replacement = replaceManifestVersion({ path, text, from, to });
/// ```
fn manifest_bump(planned: PlannedBump, state: &ManifestState) -> Result<ManifestBump, PlanError> {
let edited: String = replace_manifest_version(
&state.path,
&state.text,
&planned.bump.from,
&planned.bump.to,
)?;
let prefix: &[u8] = &state.bytes[..state.bytes.len() - state.text.len()];
return Ok(ManifestBump {
planned,
path: state.path.clone(),
mode: state.mode,
original: state.bytes.clone(),
replacement: [prefix, edited.as_bytes()].concat(),
});
}
/// What: Plan patch bumps for publishable dependents of raised packages.
/// Why: Reads every manifest at both states; the registry configuration only when a
/// version was raised; source files only of dependents whose development edges could
/// carry a raised version.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function planWorkspaceBumps(reader: WorkspaceFileReader): Promise<WorkspaceBumpPlan>;
/// ```
pub fn plan_workspace_bumps(content: &mut dyn WorkspaceContent) -> Result<PlanOutcome, PlanError> {
let tracked: Vec<TrackedPath> = content.tracked_paths()?;
let states: Vec<ManifestState> = read_manifest_states(content, &tracked)?;
let bumped: Vec<String> = bumped_names(&states);
let empty = |bumped_names: Vec<String>| {
return Ok(PlanOutcome::Planned(WorkspaceBumpPlan {
bumped_names,
bumps: Vec::new(),
}));
};
if bumped.is_empty() {
return empty(bumped);
}
let Some(publishable) = publishable_names(content, &tracked)? else {
return empty(bumped);
};
// `if let Some(...)` returns the first duplicate as the failure.
if let Some(duplicate) = duplicate_name(&states) {
return Err(PlanError::PolicyIncomplete(duplicate));
}
let bundled: Vec<Vec<String>> = bundled_development_edges(content, &tracked, &states, &bumped)?;
// `.zip` pairs each state with its bundled edges.
let nodes: Vec<WorkspaceNode> = states
.iter()
.zip(bundled)
.map(|(state, edges): (&ManifestState, Vec<String>)| {
return WorkspaceNode {
name: state.facts.name.clone(),
directory: state.directory.clone(),
version: state.facts.version.clone(),
edge_names: [state.facts.runtime_dependency_names.clone(), edges].concat(),
};
})
.collect();
let planned: Vec<PlannedBump> = match plan_dependent_bumps(&nodes, &bumped, &publishable) {
Ok(found) => found,
Err(unsupported) => return Ok(PlanOutcome::Unsupported(unsupported)),
};
let mut bumps: Vec<ManifestBump> = Vec::new();
for bump in planned {
// Names are unique once the duplicate check passed, so this finds the one state.
if let Some(state) = states
.iter()
.find(|state: &&ManifestState| return state.facts.name == bump.name)
{
bumps.push(manifest_bump(bump, state)?);
}
}
return Ok(PlanOutcome::Planned(WorkspaceBumpPlan {
bumped_names: bumped,
bumps,
}));
}
/// The plan over scripted workspaces, including the incumbent's worktree ripple cases.
#[cfg(test)]
#[path = "dependent_version_plan_tests.rs"]
mod tests;