@fuzdev/fuz_gitops 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/ModulesDetail.svelte +180 -0
  4. package/dist/ModulesDetail.svelte.d.ts +10 -0
  5. package/dist/ModulesDetail.svelte.d.ts.map +1 -0
  6. package/dist/ModulesNav.svelte +43 -0
  7. package/dist/ModulesNav.svelte.d.ts +11 -0
  8. package/dist/ModulesNav.svelte.d.ts.map +1 -0
  9. package/dist/ModulesPage.svelte +50 -0
  10. package/dist/ModulesPage.svelte.d.ts +9 -0
  11. package/dist/ModulesPage.svelte.d.ts.map +1 -0
  12. package/dist/PageFooter.svelte +15 -0
  13. package/dist/PageFooter.svelte.d.ts +19 -0
  14. package/dist/PageFooter.svelte.d.ts.map +1 -0
  15. package/dist/PageHeader.svelte +35 -0
  16. package/dist/PageHeader.svelte.d.ts +19 -0
  17. package/dist/PageHeader.svelte.d.ts.map +1 -0
  18. package/dist/PullRequestsDetail.svelte +53 -0
  19. package/dist/PullRequestsDetail.svelte.d.ts +10 -0
  20. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -0
  21. package/dist/PullRequestsPage.svelte +47 -0
  22. package/dist/PullRequestsPage.svelte.d.ts +11 -0
  23. package/dist/PullRequestsPage.svelte.d.ts.map +1 -0
  24. package/dist/ReposTable.svelte +189 -0
  25. package/dist/ReposTable.svelte.d.ts +9 -0
  26. package/dist/ReposTable.svelte.d.ts.map +1 -0
  27. package/dist/ReposTree.svelte +88 -0
  28. package/dist/ReposTree.svelte.d.ts +11 -0
  29. package/dist/ReposTree.svelte.d.ts.map +1 -0
  30. package/dist/ReposTreeNav.svelte +55 -0
  31. package/dist/ReposTreeNav.svelte.d.ts +11 -0
  32. package/dist/ReposTreeNav.svelte.d.ts.map +1 -0
  33. package/dist/TablePage.svelte +46 -0
  34. package/dist/TablePage.svelte.d.ts +9 -0
  35. package/dist/TablePage.svelte.d.ts.map +1 -0
  36. package/dist/TreeItemPage.svelte +75 -0
  37. package/dist/TreeItemPage.svelte.d.ts +10 -0
  38. package/dist/TreeItemPage.svelte.d.ts.map +1 -0
  39. package/dist/TreePage.svelte +64 -0
  40. package/dist/TreePage.svelte.d.ts +9 -0
  41. package/dist/TreePage.svelte.d.ts.map +1 -0
  42. package/dist/changeset_generator.d.ts +38 -0
  43. package/dist/changeset_generator.d.ts.map +1 -0
  44. package/dist/changeset_generator.js +110 -0
  45. package/dist/changeset_reader.d.ts +75 -0
  46. package/dist/changeset_reader.d.ts.map +1 -0
  47. package/dist/changeset_reader.js +167 -0
  48. package/dist/constants.d.ts +9 -0
  49. package/dist/constants.d.ts.map +1 -0
  50. package/dist/constants.js +8 -0
  51. package/dist/dependency_graph.d.ts +120 -0
  52. package/dist/dependency_graph.d.ts.map +1 -0
  53. package/dist/dependency_graph.js +341 -0
  54. package/dist/dependency_updater.d.ts +46 -0
  55. package/dist/dependency_updater.d.ts.map +1 -0
  56. package/dist/dependency_updater.js +213 -0
  57. package/dist/fetch_repo_data.d.ts +19 -0
  58. package/dist/fetch_repo_data.d.ts.map +1 -0
  59. package/dist/fetch_repo_data.js +49 -0
  60. package/dist/fs_fetch_value_cache.d.ts +24 -0
  61. package/dist/fs_fetch_value_cache.d.ts.map +1 -0
  62. package/dist/fs_fetch_value_cache.js +61 -0
  63. package/dist/git_operations.d.ts +54 -0
  64. package/dist/git_operations.d.ts.map +1 -0
  65. package/dist/git_operations.js +144 -0
  66. package/dist/github.d.ts +91 -0
  67. package/dist/github.d.ts.map +1 -0
  68. package/dist/github.js +94 -0
  69. package/dist/github_helpers.d.ts +10 -0
  70. package/dist/github_helpers.d.ts.map +1 -0
  71. package/dist/github_helpers.js +13 -0
  72. package/dist/gitops_analyze.task.d.ts +17 -0
  73. package/dist/gitops_analyze.task.d.ts.map +1 -0
  74. package/dist/gitops_analyze.task.js +188 -0
  75. package/dist/gitops_config.d.ts +56 -0
  76. package/dist/gitops_config.d.ts.map +1 -0
  77. package/dist/gitops_config.js +63 -0
  78. package/dist/gitops_plan.task.d.ts +28 -0
  79. package/dist/gitops_plan.task.d.ts.map +1 -0
  80. package/dist/gitops_plan.task.js +217 -0
  81. package/dist/gitops_publish.task.d.ts +29 -0
  82. package/dist/gitops_publish.task.d.ts.map +1 -0
  83. package/dist/gitops_publish.task.js +178 -0
  84. package/dist/gitops_sync.task.d.ts +18 -0
  85. package/dist/gitops_sync.task.d.ts.map +1 -0
  86. package/dist/gitops_sync.task.js +95 -0
  87. package/dist/gitops_task_helpers.d.ts +63 -0
  88. package/dist/gitops_task_helpers.d.ts.map +1 -0
  89. package/dist/gitops_task_helpers.js +84 -0
  90. package/dist/gitops_validate.task.d.ts +12 -0
  91. package/dist/gitops_validate.task.d.ts.map +1 -0
  92. package/dist/gitops_validate.task.js +210 -0
  93. package/dist/graph_validation.d.ts +39 -0
  94. package/dist/graph_validation.d.ts.map +1 -0
  95. package/dist/graph_validation.js +79 -0
  96. package/dist/local_repo.d.ts +84 -0
  97. package/dist/local_repo.d.ts.map +1 -0
  98. package/dist/local_repo.js +213 -0
  99. package/dist/log_helpers.d.ts +43 -0
  100. package/dist/log_helpers.d.ts.map +1 -0
  101. package/dist/log_helpers.js +98 -0
  102. package/dist/multi_repo_publisher.d.ts +34 -0
  103. package/dist/multi_repo_publisher.d.ts.map +1 -0
  104. package/dist/multi_repo_publisher.js +364 -0
  105. package/dist/npm_install_helpers.d.ts +23 -0
  106. package/dist/npm_install_helpers.d.ts.map +1 -0
  107. package/dist/npm_install_helpers.js +60 -0
  108. package/dist/npm_registry.d.ts +46 -0
  109. package/dist/npm_registry.d.ts.map +1 -0
  110. package/dist/npm_registry.js +96 -0
  111. package/dist/operations.d.ts +409 -0
  112. package/dist/operations.d.ts.map +1 -0
  113. package/dist/operations.js +34 -0
  114. package/dist/operations_defaults.d.ts +19 -0
  115. package/dist/operations_defaults.d.ts.map +1 -0
  116. package/dist/operations_defaults.js +279 -0
  117. package/dist/output_helpers.d.ts +27 -0
  118. package/dist/output_helpers.d.ts.map +1 -0
  119. package/dist/output_helpers.js +39 -0
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +10 -0
  123. package/dist/preflight_checks.d.ts +47 -0
  124. package/dist/preflight_checks.d.ts.map +1 -0
  125. package/dist/preflight_checks.js +181 -0
  126. package/dist/publishing_plan.d.ts +100 -0
  127. package/dist/publishing_plan.d.ts.map +1 -0
  128. package/dist/publishing_plan.js +353 -0
  129. package/dist/publishing_plan_helpers.d.ts +30 -0
  130. package/dist/publishing_plan_helpers.d.ts.map +1 -0
  131. package/dist/publishing_plan_helpers.js +112 -0
  132. package/dist/publishing_plan_logging.d.ts +18 -0
  133. package/dist/publishing_plan_logging.d.ts.map +1 -0
  134. package/dist/publishing_plan_logging.js +342 -0
  135. package/dist/repo.svelte.d.ts +52 -0
  136. package/dist/repo.svelte.d.ts.map +1 -0
  137. package/dist/repo.svelte.js +70 -0
  138. package/dist/repo_ops.d.ts +57 -0
  139. package/dist/repo_ops.d.ts.map +1 -0
  140. package/dist/repo_ops.js +167 -0
  141. package/dist/resolved_gitops_config.d.ts +9 -0
  142. package/dist/resolved_gitops_config.d.ts.map +1 -0
  143. package/dist/resolved_gitops_config.js +12 -0
  144. package/dist/semver.d.ts +24 -0
  145. package/dist/semver.d.ts.map +1 -0
  146. package/dist/semver.js +140 -0
  147. package/dist/serialization_types.d.ts +57 -0
  148. package/dist/serialization_types.d.ts.map +1 -0
  149. package/dist/serialization_types.js +40 -0
  150. package/dist/version_utils.d.ts +48 -0
  151. package/dist/version_utils.d.ts.map +1 -0
  152. package/dist/version_utils.js +125 -0
  153. package/package.json +107 -0
  154. package/src/lib/changeset_generator.ts +162 -0
  155. package/src/lib/changeset_reader.ts +218 -0
  156. package/src/lib/constants.ts +8 -0
  157. package/src/lib/dependency_graph.ts +423 -0
  158. package/src/lib/dependency_updater.ts +297 -0
  159. package/src/lib/fetch_repo_data.ts +64 -0
  160. package/src/lib/fs_fetch_value_cache.ts +75 -0
  161. package/src/lib/git_operations.ts +208 -0
  162. package/src/lib/github.ts +128 -0
  163. package/src/lib/github_helpers.ts +31 -0
  164. package/src/lib/gitops_analyze.task.ts +261 -0
  165. package/src/lib/gitops_config.ts +123 -0
  166. package/src/lib/gitops_plan.task.ts +272 -0
  167. package/src/lib/gitops_publish.task.ts +227 -0
  168. package/src/lib/gitops_sync.task.ts +109 -0
  169. package/src/lib/gitops_task_helpers.ts +126 -0
  170. package/src/lib/gitops_validate.task.ts +248 -0
  171. package/src/lib/graph_validation.ts +109 -0
  172. package/src/lib/local_repo.ts +359 -0
  173. package/src/lib/log_helpers.ts +147 -0
  174. package/src/lib/multi_repo_publisher.ts +464 -0
  175. package/src/lib/npm_install_helpers.ts +85 -0
  176. package/src/lib/npm_registry.ts +143 -0
  177. package/src/lib/operations.ts +334 -0
  178. package/src/lib/operations_defaults.ts +335 -0
  179. package/src/lib/output_helpers.ts +64 -0
  180. package/src/lib/paths.ts +11 -0
  181. package/src/lib/preflight_checks.ts +269 -0
  182. package/src/lib/publishing_plan.ts +531 -0
  183. package/src/lib/publishing_plan_helpers.ts +145 -0
  184. package/src/lib/publishing_plan_logging.ts +470 -0
  185. package/src/lib/repo.svelte.ts +95 -0
  186. package/src/lib/repo_ops.ts +213 -0
  187. package/src/lib/resolved_gitops_config.ts +27 -0
  188. package/src/lib/semver.ts +166 -0
  189. package/src/lib/serialization_types.ts +90 -0
  190. package/src/lib/version_utils.ts +150 -0
@@ -0,0 +1,531 @@
1
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
2
+ import {styleText as st} from 'node:util';
3
+
4
+ import type {LocalRepo} from './local_repo.js';
5
+ import type {BumpType} from './semver.js';
6
+ import {validate_dependency_graph} from './graph_validation.js';
7
+ import {is_breaking_change, compare_bump_types, calculate_next_version} from './version_utils.js';
8
+ import type {ChangesetOperations} from './operations.js';
9
+ import {default_changeset_operations} from './operations_defaults.js';
10
+ import {MAX_ITERATIONS} from './constants.js';
11
+ import type {DependencyGraph} from './dependency_graph.ts';
12
+ import {
13
+ calculate_dependency_updates,
14
+ get_required_bump_for_dependencies,
15
+ } from './publishing_plan_helpers.js';
16
+
17
+ // Re-export logging functions
18
+ export {log_publishing_plan, type LogPlanOptions} from './publishing_plan_logging.js';
19
+
20
+ export interface VersionChange {
21
+ package_name: string;
22
+ from: string;
23
+ to: string;
24
+ bump_type: BumpType;
25
+ breaking: boolean;
26
+ has_changesets: boolean;
27
+ will_generate_changeset?: boolean; // True if changeset will be auto-generated for dependency updates
28
+ needs_bump_escalation?: boolean; // True if existing changesets need escalation for dependencies
29
+ existing_bump?: BumpType; // The bump type from existing changesets
30
+ required_bump?: BumpType; // The required bump type from dependencies
31
+ }
32
+
33
+ export interface DependencyUpdate {
34
+ dependent_package: string;
35
+ updated_dependency: string;
36
+ current_version: string;
37
+ new_version: string;
38
+ type: 'dependencies' | 'devDependencies' | 'peerDependencies';
39
+ causes_republish: boolean;
40
+ }
41
+
42
+ // Verbose data types for diagnostic output
43
+ export interface VerboseChangesetDetail {
44
+ package_name: string;
45
+ files: Array<{filename: string; bump_type: BumpType; summary: string}>;
46
+ }
47
+
48
+ export interface VerboseIterationPackage {
49
+ name: string;
50
+ changeset_count: number;
51
+ bump_from_changesets: BumpType | null;
52
+ required_bump: BumpType | null;
53
+ triggering_dep: string | null;
54
+ action: 'publish' | 'auto_changeset' | 'escalation' | 'skip';
55
+ version_to: string | null;
56
+ is_breaking: boolean;
57
+ }
58
+
59
+ export interface VerboseIteration {
60
+ iteration: number;
61
+ packages: Array<VerboseIterationPackage>;
62
+ new_changes: number;
63
+ }
64
+
65
+ export interface VerbosePropagationChain {
66
+ source: string;
67
+ chain: Array<{pkg: string; dep_type: 'prod' | 'peer'; action: string}>;
68
+ }
69
+
70
+ export interface VerboseGraphSummary {
71
+ package_count: number;
72
+ internal_dep_count: number;
73
+ prod_peer_edges: Array<{from: string; to: string; type: 'prod' | 'peer'}>;
74
+ dev_edges: Array<{from: string; to: string}>;
75
+ prod_cycle_count: number;
76
+ dev_cycle_count: number;
77
+ }
78
+
79
+ export interface VerboseData {
80
+ changeset_details: Array<VerboseChangesetDetail>;
81
+ iterations: Array<VerboseIteration>;
82
+ propagation_chains: Array<VerbosePropagationChain>;
83
+ graph_summary: VerboseGraphSummary;
84
+ total_iterations: number;
85
+ }
86
+
87
+ export interface PublishingPlan {
88
+ publishing_order: Array<string>;
89
+ version_changes: Array<VersionChange>;
90
+ dependency_updates: Array<DependencyUpdate>;
91
+ breaking_cascades: Map<string, Array<string>>;
92
+ warnings: Array<string>;
93
+ info: Array<string>; // Informational status (not warnings)
94
+ errors: Array<string>;
95
+ verbose_data?: VerboseData;
96
+ }
97
+
98
+ export interface GeneratePlanOptions {
99
+ log?: Logger;
100
+ ops?: ChangesetOperations;
101
+ verbose?: boolean;
102
+ }
103
+
104
+ /**
105
+ * Generates a publishing plan showing what would happen during publishing.
106
+ * Shows version changes, dependency updates, and breaking change cascades.
107
+ * Uses fixed-point iteration to resolve transitive cascades.
108
+ */
109
+ export const generate_publishing_plan = async (
110
+ repos: Array<LocalRepo>,
111
+ options: GeneratePlanOptions = {},
112
+ ): Promise<PublishingPlan> => {
113
+ const {log, ops = default_changeset_operations, verbose} = options;
114
+ log?.info(st('cyan', 'Generating publishing plan...'));
115
+
116
+ const warnings: Array<string> = [];
117
+ const info: Array<string> = []; // Informational status (not warnings)
118
+ const errors: Array<string> = [];
119
+
120
+ // Verbose data collection
121
+ const verbose_changeset_details: Array<VerboseChangesetDetail> = [];
122
+ const verbose_iterations: Array<VerboseIteration> = [];
123
+
124
+ // Build dependency graph and validate
125
+ let publishing_order: Array<string>;
126
+ let production_cycles: Array<Array<string>>;
127
+ let dev_cycles: Array<Array<string>>;
128
+ let graph: DependencyGraph | null = null;
129
+
130
+ try {
131
+ const validation = validate_dependency_graph(repos, {
132
+ throw_on_prod_cycles: false, // Collect errors instead of throwing
133
+ log_cycles: false, // We'll handle our own error collection
134
+ log_order: false, // Plan generation doesn't need to log order
135
+ });
136
+ publishing_order = validation.publishing_order;
137
+ production_cycles = validation.production_cycles;
138
+ dev_cycles = validation.dev_cycles;
139
+ graph = validation.graph; // Store for verbose output
140
+
141
+ // Add topological sort error if present
142
+ if (validation.sort_error) {
143
+ errors.push(validation.sort_error);
144
+ }
145
+ } catch (error) {
146
+ errors.push(`Failed to validate dependency graph: ${error}`);
147
+ return {
148
+ publishing_order: [],
149
+ version_changes: [],
150
+ dependency_updates: [],
151
+ breaking_cascades: new Map(),
152
+ warnings,
153
+ info,
154
+ errors,
155
+ };
156
+ }
157
+
158
+ // Collect cycle errors
159
+ if (production_cycles.length > 0) {
160
+ for (const cycle of production_cycles) {
161
+ errors.push(`Production dependency cycle: ${cycle.join(' → ')}`);
162
+ }
163
+ }
164
+
165
+ // Dev cycles are shown in gitops_analyze, not repeated here
166
+ if (dev_cycles.length > 0) {
167
+ info.push(
168
+ `${dev_cycles.length} dev dependency cycle(s) detected (normal, shown in gitops_analyze)`,
169
+ );
170
+ }
171
+
172
+ // Initial pass: get all packages with explicit changesets
173
+ const predicted_versions: Map<string, string> = new Map();
174
+ const breaking_packages: Set<string> = new Set();
175
+ const version_changes: Array<VersionChange> = [];
176
+
177
+ for (const pkg_name of publishing_order) {
178
+ const repo = repos.find((r) => r.library.name === pkg_name);
179
+ if (!repo) continue;
180
+
181
+ // Check for changesets
182
+ const has_result = await ops.has_changesets({repo}); // eslint-disable-line no-await-in-loop
183
+
184
+ if (!has_result.ok) {
185
+ errors.push(`Failed to check changesets for ${pkg_name}: ${has_result.message}`);
186
+ continue;
187
+ }
188
+
189
+ if (has_result.value) {
190
+ // Predict version from changesets
191
+ const prediction = await ops.predict_next_version({repo, log}); // eslint-disable-line no-await-in-loop
192
+
193
+ if (!prediction) {
194
+ // No changesets found - this shouldn't happen since has_changesets returned true
195
+ continue;
196
+ }
197
+
198
+ if (!prediction.ok) {
199
+ errors.push(`Failed to predict version for ${pkg_name}: ${prediction.message}`);
200
+ continue;
201
+ }
202
+
203
+ // Capture changeset details for verbose output
204
+ if (verbose) {
205
+ const changesets_result = await ops.read_changesets({repo, log}); // eslint-disable-line no-await-in-loop
206
+ if (changesets_result.ok) {
207
+ const files = changesets_result.value
208
+ .filter((cs) => cs.packages.some((p) => p.name === pkg_name))
209
+ .map((cs) => {
210
+ const pkg_entry = cs.packages.find((p) => p.name === pkg_name);
211
+ return {
212
+ filename: cs.filename,
213
+ bump_type: pkg_entry?.bump_type || prediction.bump_type,
214
+ summary: cs.summary,
215
+ };
216
+ });
217
+ if (files.length > 0) {
218
+ verbose_changeset_details.push({package_name: pkg_name, files});
219
+ }
220
+ }
221
+ }
222
+
223
+ {
224
+ const old_version = repo.library.package_json.version || '0.0.0';
225
+ const is_breaking = is_breaking_change(old_version, prediction.bump_type);
226
+
227
+ predicted_versions.set(pkg_name, prediction.version);
228
+
229
+ if (is_breaking) {
230
+ breaking_packages.add(pkg_name);
231
+ }
232
+
233
+ version_changes.push({
234
+ package_name: pkg_name,
235
+ from: old_version,
236
+ to: prediction.version,
237
+ bump_type: prediction.bump_type,
238
+ breaking: is_breaking,
239
+ has_changesets: true,
240
+ });
241
+ }
242
+ }
243
+ }
244
+
245
+ // Fixed-point iteration to resolve transitive cascades
246
+ // Loop until no new version changes are discovered
247
+ let iteration = 0;
248
+ let changed = true;
249
+
250
+ while (changed && iteration < MAX_ITERATIONS) {
251
+ changed = false;
252
+ iteration++;
253
+
254
+ // Verbose iteration tracking
255
+ const verbose_iteration_packages: Array<VerboseIterationPackage> = [];
256
+ let verbose_new_changes = 0;
257
+
258
+ // Recalculate dependency updates based on current predicted versions
259
+ // (breaking_cascades not needed during iteration, only calculated at the end)
260
+ const {dependency_updates} = calculate_dependency_updates(
261
+ repos,
262
+ predicted_versions,
263
+ breaking_packages,
264
+ );
265
+
266
+ // Process packages to check for bump escalation and auto-generated changesets
267
+ for (const repo of repos) {
268
+ const pkg_name = repo.library.name;
269
+
270
+ // Get required bump from dependencies
271
+ const required_bump = get_required_bump_for_dependencies(
272
+ repo,
273
+ dependency_updates,
274
+ breaking_packages,
275
+ );
276
+
277
+ // Find triggering dependency for verbose output
278
+ let triggering_dep: string | null = null;
279
+ if (required_bump) {
280
+ const relevant_updates = dependency_updates.filter(
281
+ (u) =>
282
+ u.dependent_package === pkg_name &&
283
+ (u.type === 'dependencies' || u.type === 'peerDependencies') &&
284
+ breaking_packages.has(u.updated_dependency),
285
+ );
286
+ if (relevant_updates.length > 0) {
287
+ triggering_dep = `${relevant_updates[0]!.updated_dependency} BREAKING`;
288
+ }
289
+ }
290
+
291
+ // Check if already in version_changes (has changesets)
292
+ const existing_entry = version_changes.find((vc) => vc.package_name === pkg_name);
293
+
294
+ if (existing_entry) {
295
+ // Package has changesets - check if it needs bump escalation
296
+ if (required_bump && compare_bump_types(required_bump, existing_entry.bump_type) > 0) {
297
+ // Dependencies require a larger bump than existing changesets provide
298
+ const old_version = repo.library.package_json.version || '0.0.0';
299
+ const new_version = calculate_next_version(old_version, required_bump);
300
+
301
+ // Only mark as changed if version actually changed
302
+ if (existing_entry.to !== new_version) {
303
+ changed = true;
304
+ verbose_new_changes++;
305
+
306
+ // Capture verbose data before updating
307
+ if (verbose) {
308
+ const changeset_detail = verbose_changeset_details.find(
309
+ (d) => d.package_name === pkg_name,
310
+ );
311
+ verbose_iteration_packages.push({
312
+ name: pkg_name,
313
+ changeset_count: changeset_detail?.files.length || 1,
314
+ bump_from_changesets: existing_entry.bump_type,
315
+ required_bump,
316
+ triggering_dep,
317
+ action: 'escalation',
318
+ version_to: new_version,
319
+ is_breaking: is_breaking_change(old_version, required_bump),
320
+ });
321
+ }
322
+
323
+ existing_entry.needs_bump_escalation = true;
324
+ existing_entry.existing_bump = existing_entry.bump_type;
325
+ existing_entry.required_bump = required_bump;
326
+ existing_entry.bump_type = required_bump;
327
+ existing_entry.to = new_version;
328
+ existing_entry.breaking = is_breaking_change(old_version, required_bump);
329
+
330
+ // Update predicted versions
331
+ predicted_versions.set(pkg_name, new_version);
332
+
333
+ if (existing_entry.breaking) {
334
+ breaking_packages.add(pkg_name);
335
+ }
336
+ }
337
+ }
338
+ } else if (required_bump) {
339
+ // No existing changesets but needs changeset for dependency updates
340
+ const old_version = repo.library.package_json.version || '0.0.0';
341
+ const new_version = calculate_next_version(old_version, required_bump);
342
+
343
+ // Check if this is a new version (not already in version_changes)
344
+ if (!predicted_versions.has(pkg_name)) {
345
+ changed = true;
346
+ verbose_new_changes++;
347
+
348
+ const is_breaking = is_breaking_change(old_version, required_bump);
349
+
350
+ // Capture verbose data
351
+ if (verbose) {
352
+ verbose_iteration_packages.push({
353
+ name: pkg_name,
354
+ changeset_count: 0,
355
+ bump_from_changesets: null,
356
+ required_bump,
357
+ triggering_dep,
358
+ action: 'auto_changeset',
359
+ version_to: new_version,
360
+ is_breaking,
361
+ });
362
+ }
363
+
364
+ if (is_breaking) {
365
+ breaking_packages.add(pkg_name);
366
+ }
367
+
368
+ version_changes.push({
369
+ package_name: pkg_name,
370
+ from: old_version,
371
+ to: new_version,
372
+ bump_type: required_bump,
373
+ breaking: is_breaking,
374
+ has_changesets: false,
375
+ will_generate_changeset: true,
376
+ });
377
+
378
+ // Update predicted versions
379
+ predicted_versions.set(pkg_name, new_version);
380
+ }
381
+ }
382
+ }
383
+
384
+ // Store verbose iteration data
385
+ if (verbose && (verbose_iteration_packages.length > 0 || iteration === 1)) {
386
+ verbose_iterations.push({
387
+ iteration,
388
+ packages: verbose_iteration_packages,
389
+ new_changes: verbose_new_changes,
390
+ });
391
+ }
392
+ }
393
+
394
+ // Check if we hit iteration limit without convergence
395
+ if (iteration === MAX_ITERATIONS && changed) {
396
+ // Calculate how many packages still need processing
397
+ const pending_packages: Array<string> = [];
398
+
399
+ // Recalculate one more time to see what's pending
400
+ const {dependency_updates: pending_updates} = calculate_dependency_updates(
401
+ repos,
402
+ predicted_versions,
403
+ breaking_packages,
404
+ );
405
+
406
+ for (const repo of repos) {
407
+ const pkg_name = repo.library.name;
408
+ const required_bump = get_required_bump_for_dependencies(
409
+ repo,
410
+ pending_updates,
411
+ breaking_packages,
412
+ );
413
+
414
+ // Check if this package would need processing
415
+ const existing_entry = version_changes.find((vc) => vc.package_name === pkg_name);
416
+ const needs_escalation =
417
+ existing_entry &&
418
+ required_bump &&
419
+ compare_bump_types(required_bump, existing_entry.bump_type) > 0;
420
+ const needs_auto_changeset = !existing_entry && required_bump;
421
+
422
+ if (needs_escalation || needs_auto_changeset) {
423
+ pending_packages.push(pkg_name);
424
+ }
425
+ }
426
+
427
+ // Add warning with diagnostics
428
+ const pending_count = pending_packages.length;
429
+ const estimated_iterations = Math.ceil(pending_count / 2); // Rough estimate
430
+ warnings.push(
431
+ `Reached maximum iterations (${MAX_ITERATIONS}) without full convergence - ` +
432
+ `${pending_count} package(s) may still need processing: ${pending_packages.join(', ')}. ` +
433
+ `Estimated ${estimated_iterations} more iteration(s) needed.`,
434
+ );
435
+ }
436
+
437
+ // Final dependency updates calculation after convergence
438
+ const {dependency_updates, breaking_cascades} = calculate_dependency_updates(
439
+ repos,
440
+ predicted_versions,
441
+ breaking_packages,
442
+ );
443
+
444
+ // Identify packages with no changes
445
+ for (const repo of repos) {
446
+ const has_version_change = version_changes.some((vc) => vc.package_name === repo.library.name);
447
+ if (!has_version_change) {
448
+ const has_result = await ops.has_changesets({repo}); // eslint-disable-line no-await-in-loop
449
+ if (has_result.ok && !has_result.value) {
450
+ info.push(repo.library.name);
451
+ }
452
+ }
453
+ }
454
+
455
+ // Build verbose data if requested
456
+ let verbose_data: VerboseData | undefined;
457
+ if (verbose) {
458
+ // Build propagation chains from breaking_cascades
459
+ const propagation_chains: Array<VerbosePropagationChain> = [];
460
+ for (const [source, affected] of breaking_cascades) {
461
+ const chain: Array<{pkg: string; dep_type: 'prod' | 'peer'; action: string}> = [];
462
+ for (const pkg of affected) {
463
+ // Determine dep type and action
464
+ const update = dependency_updates.find(
465
+ (u) => u.dependent_package === pkg && u.updated_dependency === source,
466
+ );
467
+ const dep_type: 'prod' | 'peer' = update?.type === 'peerDependencies' ? 'peer' : 'prod';
468
+ const version_change = version_changes.find((vc) => vc.package_name === pkg);
469
+ let action = 'update';
470
+ if (version_change?.will_generate_changeset) {
471
+ action = 'auto-changeset';
472
+ } else if (version_change?.needs_bump_escalation) {
473
+ action = 'bump escalation';
474
+ }
475
+ chain.push({pkg, dep_type, action});
476
+ }
477
+ if (chain.length > 0) {
478
+ propagation_chains.push({source, chain});
479
+ }
480
+ }
481
+
482
+ // Build graph summary
483
+ const prod_peer_edges: Array<{from: string; to: string; type: 'prod' | 'peer'}> = [];
484
+ const dev_edges: Array<{from: string; to: string}> = [];
485
+ let internal_dep_count = 0;
486
+
487
+ for (const [pkg_name, node] of graph.nodes) {
488
+ for (const [dep_name, spec] of node.dependencies) {
489
+ // Only count internal dependencies (deps that are also in the graph)
490
+ if (graph.nodes.has(dep_name)) {
491
+ internal_dep_count++;
492
+ if (spec.type === 'dev') {
493
+ dev_edges.push({from: pkg_name, to: dep_name});
494
+ } else {
495
+ prod_peer_edges.push({
496
+ from: pkg_name,
497
+ to: dep_name,
498
+ type: spec.type === 'peer' ? 'peer' : 'prod',
499
+ });
500
+ }
501
+ }
502
+ }
503
+ }
504
+
505
+ verbose_data = {
506
+ changeset_details: verbose_changeset_details,
507
+ iterations: verbose_iterations,
508
+ propagation_chains,
509
+ graph_summary: {
510
+ package_count: graph.nodes.size,
511
+ internal_dep_count,
512
+ prod_peer_edges,
513
+ dev_edges,
514
+ prod_cycle_count: production_cycles.length,
515
+ dev_cycle_count: dev_cycles.length,
516
+ },
517
+ total_iterations: iteration,
518
+ };
519
+ }
520
+
521
+ return {
522
+ publishing_order,
523
+ version_changes,
524
+ dependency_updates,
525
+ breaking_cascades,
526
+ warnings,
527
+ info,
528
+ errors,
529
+ verbose_data,
530
+ };
531
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Helper functions for publishing plan calculations.
3
+ *
4
+ * Extracted from publishing_plan.ts to reduce file size.
5
+ */
6
+
7
+ import type {LocalRepo} from './local_repo.js';
8
+ import type {BumpType} from './semver.js';
9
+ import {needs_update} from './version_utils.js';
10
+ import type {DependencyUpdate} from './publishing_plan.js';
11
+
12
+ /**
13
+ * Calculates all dependency updates between packages based on predicted versions.
14
+ *
15
+ * Iterates through all repos, checking prod, peer, and dev dependencies to find
16
+ * which packages will need dependency version bumps after publishing.
17
+ *
18
+ * Also tracks "breaking cascades" - when a breaking change propagates to dependents.
19
+ */
20
+ export const calculate_dependency_updates = (
21
+ repos: Array<LocalRepo>,
22
+ predicted_versions: Map<string, string>,
23
+ breaking_packages: Set<string>,
24
+ ): {
25
+ dependency_updates: Array<DependencyUpdate>;
26
+ breaking_cascades: Map<string, Array<string>>;
27
+ } => {
28
+ const dependency_updates: Array<DependencyUpdate> = [];
29
+ const breaking_cascades: Map<string, Array<string>> = new Map();
30
+
31
+ for (const repo of repos) {
32
+ // Check prod dependencies
33
+ if (repo.dependencies) {
34
+ for (const [dep_name, current_version] of repo.dependencies) {
35
+ const new_version = predicted_versions.get(dep_name);
36
+ if (new_version && needs_update(current_version, new_version)) {
37
+ dependency_updates.push({
38
+ dependent_package: repo.library.name,
39
+ updated_dependency: dep_name,
40
+ current_version,
41
+ new_version,
42
+ type: 'dependencies',
43
+ causes_republish: true,
44
+ });
45
+
46
+ if (breaking_packages.has(dep_name)) {
47
+ const cascades = breaking_cascades.get(dep_name) || [];
48
+ if (!cascades.includes(repo.library.name)) {
49
+ cascades.push(repo.library.name);
50
+ }
51
+ breaking_cascades.set(dep_name, cascades);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ // Check peer dependencies
58
+ if (repo.peer_dependencies) {
59
+ for (const [dep_name, current_version] of repo.peer_dependencies) {
60
+ const new_version = predicted_versions.get(dep_name);
61
+ if (new_version && needs_update(current_version, new_version)) {
62
+ dependency_updates.push({
63
+ dependent_package: repo.library.name,
64
+ updated_dependency: dep_name,
65
+ current_version,
66
+ new_version,
67
+ type: 'peerDependencies',
68
+ causes_republish: true,
69
+ });
70
+
71
+ if (breaking_packages.has(dep_name)) {
72
+ const cascades = breaking_cascades.get(dep_name) || [];
73
+ if (!cascades.includes(repo.library.name)) {
74
+ cascades.push(repo.library.name);
75
+ }
76
+ breaking_cascades.set(dep_name, cascades);
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ // Check dev dependencies
83
+ if (repo.dev_dependencies) {
84
+ for (const [dep_name, current_version] of repo.dev_dependencies) {
85
+ const new_version = predicted_versions.get(dep_name);
86
+ if (new_version && needs_update(current_version, new_version)) {
87
+ dependency_updates.push({
88
+ dependent_package: repo.library.name,
89
+ updated_dependency: dep_name,
90
+ current_version,
91
+ new_version,
92
+ type: 'devDependencies',
93
+ causes_republish: false,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return {dependency_updates, breaking_cascades};
101
+ };
102
+
103
+ /**
104
+ * Determines the required bump type for a package based on its dependency updates.
105
+ *
106
+ * Returns null if no prod/peer dependency updates, otherwise returns the minimum
107
+ * required bump type (major for breaking deps, patch otherwise).
108
+ *
109
+ * Respects pre-1.0 semver conventions (minor for breaking in 0.x).
110
+ */
111
+ export const get_required_bump_for_dependencies = (
112
+ repo: LocalRepo,
113
+ dependency_updates: Array<DependencyUpdate>,
114
+ breaking_packages: Set<string>,
115
+ ): BumpType | null => {
116
+ // Check if this repo has any prod/peer dependency updates
117
+ const relevant_updates = dependency_updates.filter(
118
+ (update) =>
119
+ update.dependent_package === repo.library.name &&
120
+ (update.type === 'dependencies' || update.type === 'peerDependencies'),
121
+ );
122
+
123
+ if (relevant_updates.length === 0) {
124
+ return null;
125
+ }
126
+
127
+ // Check if any of these dependencies have breaking changes
128
+ const has_breaking_deps = relevant_updates.some((update) =>
129
+ breaking_packages.has(update.updated_dependency),
130
+ );
131
+
132
+ const current_version = repo.library.package_json.version || '0.0.0';
133
+ const [major] = current_version.split('.').map(Number);
134
+ const is_pre_1_0 = major === 0;
135
+
136
+ if (has_breaking_deps) {
137
+ // Breaking changes propagate
138
+ // Pre-1.0: use minor for breaking changes
139
+ // 1.0+: use major for breaking changes
140
+ return is_pre_1_0 ? 'minor' : 'major';
141
+ }
142
+
143
+ // For non-breaking dependency updates, use patch
144
+ return 'patch';
145
+ };