@exadev/semantic-release-workspace 0.0.0 → 1.0.1

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,805 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_fs_promises = require("node:fs/promises");
25
+ let node_path = require("node:path");
26
+ let tinyglobby = require("tinyglobby");
27
+ let yaml = require("yaml");
28
+ let node_child_process = require("node:child_process");
29
+ let node_util = require("node:util");
30
+ let validate_npm_package_name = require("validate-npm-package-name");
31
+ validate_npm_package_name = __toESM(validate_npm_package_name, 1);
32
+ let node_module = require("node:module");
33
+ let _semantic_release_commit_analyzer = require("@semantic-release/commit-analyzer");
34
+ let _semantic_release_release_notes_generator = require("@semantic-release/release-notes-generator");
35
+ let semantic_release = require("semantic-release");
36
+ semantic_release = __toESM(semantic_release, 1);
37
+ //#region src/package-name.ts
38
+ const packageName = "@exadev/semantic-release-workspace";
39
+ //#endregion
40
+ //#region src/errors.ts
41
+ /**
42
+ * Every failure this package raises deliberately is one of these, so a caller (or the CLI) can tell an orchestration failure it should report cleanly apart from an unexpected crash it should let propagate with a stack trace.
43
+ *
44
+ * All of them are thrown, never returned as a status: the orchestrator deliberately has no "skip this package and carry on" path. A workspace that can't be discovered, ordered, or bumped correctly would otherwise publish a partially-consistent set of packages, which is strictly worse than publishing nothing.
45
+ */
46
+ var WorkspaceReleaseError = class extends Error {
47
+ constructor(message) {
48
+ super(message);
49
+ this.name = new.target.name;
50
+ }
51
+ };
52
+ /** The workspace itself could not be read: no `pnpm-workspace.yaml`, no `packages` globs, an unreadable or malformed `package.json`, two packages claiming the same name, or a package sitting at the workspace root (which cannot be path-scoped -- see `discoverWorkspace`). */
53
+ var WorkspaceDiscoveryError = class extends WorkspaceReleaseError {};
54
+ /** The intra-workspace dependency graph contains a cycle, so no release order exists in which every package releases after its own dependencies. */
55
+ var DependencyCycleError = class extends WorkspaceReleaseError {
56
+ /** The packages forming the cycle, in dependency order, with the first package repeated at the end so the loop reads end to end. */
57
+ cycle;
58
+ constructor(cycle) {
59
+ super(`Cannot compute a release order: the workspace dependency graph contains a cycle: ${cycle.join(" -> ")}`);
60
+ this.cycle = cycle;
61
+ }
62
+ };
63
+ /** A dependency on a workspace sibling uses a range this tool cannot rewrite with confidence. Rewriting it wrongly, or leaving it silently stale, both produce a published manifest that disagrees with the repository, so the run stops instead. */
64
+ var UnsupportedDependencyRangeError = class extends WorkspaceReleaseError {};
65
+ /** The semantic-release options handed to the orchestrator cannot be scoped to a single package -- typically a publish plugin list that would leave a release commit or a cross-package manifest bump uncommitted. */
66
+ var ReleaseConfigurationError = class extends WorkspaceReleaseError {};
67
+ /** A git command the orchestrator runs itself (history filtering, dependency-bump commits, pushes) failed. Carries the exit code so callers can distinguish "configuration is missing" (exit 1) from real repository failures. */
68
+ var GitCommandError = class extends WorkspaceReleaseError {
69
+ exitCode;
70
+ constructor(args, cwd, exitCode, detail) {
71
+ super(`git ${args.join(" ")} failed in ${cwd}${exitCode === void 0 ? "" : ` (exit ${exitCode})`}: ${detail}`);
72
+ this.exitCode = exitCode;
73
+ }
74
+ };
75
+ /** The workspace's git state does not support the release operation -- for example a detached HEAD, which names no branch that dependency-bump commits could be pushed to. */
76
+ var WorkspaceStateError = class extends WorkspaceReleaseError {};
77
+ //#endregion
78
+ //#region src/git.ts
79
+ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
80
+ /** `git log --name-only` over everything since a package's last release tag can legitimately produce tens of megabytes of path output on a long-lived monorepo, well past execFile's default buffer, failing on exactly the big workspaces this tool exists for. */
81
+ const GIT_MAX_BUFFER_BYTES = 104857600;
82
+ /** Separates one commit's record in `git log --format` output. Chosen from the C0 control range so it can never appear in a hash or a file path. */
83
+ const COMMIT_RECORD_SEPARATOR = "";
84
+ /** The identity semantic-release's own core writes release commits under in CI when nothing else is configured (its COMMIT_NAME/COMMIT_EMAIL constants); dependency-bump commits use the same fallback so every commit a release run produces has a consistent author when the repository declares none. */
85
+ const BOT_IDENTITY = {
86
+ name: "semantic-release-bot",
87
+ email: "semantic-release-bot@martynus.net"
88
+ };
89
+ async function git(args, options) {
90
+ try {
91
+ const { stdout } = await execFileAsync("git", [...args], {
92
+ cwd: options.cwd,
93
+ maxBuffer: GIT_MAX_BUFFER_BYTES
94
+ });
95
+ return stdout;
96
+ } catch (cause) {
97
+ throw toGitCommandError(args, options.cwd, cause);
98
+ }
99
+ }
100
+ /**
101
+ * Maps every commit in `from..HEAD` (or the whole history when `from` is undefined) to the set of paths it changed, by running `git log --name-only` once per package release -- the same diff-and-filter technique the design calls for, so a commit counts for a package only when a path under that package's directory appears in its file list.
102
+ *
103
+ * Merge commits list no files (git shows no diff for them without `--diff-merges`), so they count for no package; their changes arrive through their parents, which the same range covers individually. Squash-merge workflows are unaffected, since a squash commit is an ordinary commit with a full file list.
104
+ */
105
+ async function changedPathsSince(from, options) {
106
+ const range = from === void 0 ? "HEAD" : `${from}..HEAD`;
107
+ return parseChangedPaths(await git([
108
+ "-c",
109
+ "core.quotePath=false",
110
+ "log",
111
+ "--name-only",
112
+ "--no-renames",
113
+ `--format=${COMMIT_RECORD_SEPARATOR}%H`,
114
+ range
115
+ ], options));
116
+ }
117
+ function parseChangedPaths(output) {
118
+ const changedPaths = /* @__PURE__ */ new Map();
119
+ for (const record of output.split(COMMIT_RECORD_SEPARATOR)) {
120
+ const trimmed = record.trim();
121
+ if (trimmed === "") continue;
122
+ const lines = trimmed.split("\n");
123
+ const hash = lines[0];
124
+ if (hash === void 0 || hash === "") continue;
125
+ changedPaths.set(hash, new Set(lines.slice(1).filter((line) => line !== "")));
126
+ }
127
+ return changedPaths;
128
+ }
129
+ /** The branch a dependency-bump commit will be pushed to. Refuses a detached HEAD by name rather than pushing `HEAD:HEAD` to the remote and watching it fail somewhere less legible. A detached HEAD is a WorkspaceStateError rather than a GitCommandError because the git command itself succeeded: the repository's state is what cannot support the release, and conflating the two would report a working command as a failed one. */
130
+ async function currentBranch(options) {
131
+ const branch = (await git([
132
+ "rev-parse",
133
+ "--abbrev-ref",
134
+ "HEAD"
135
+ ], options)).trim();
136
+ if (branch === "HEAD") throw new WorkspaceStateError(`HEAD is detached in ${options.cwd}; there is no branch name to push dependency-bump commits to. Run the release from a branch checkout.`);
137
+ return branch;
138
+ }
139
+ /** The commit identity for dependency-bump commits: whatever the repository itself configures, and semantic-release's own bot identity when nothing is (a release run's commits must name an author even on a bare CI runner). */
140
+ async function resolveCommitIdentity(options) {
141
+ const name = await readConfig("user.name", options);
142
+ const email = await readConfig("user.email", options);
143
+ if (name === void 0 || email === void 0) return BOT_IDENTITY;
144
+ return {
145
+ name,
146
+ email
147
+ };
148
+ }
149
+ /** `git config` exits 1 when a key is simply unset -- that is an expected answer here, not a failure; any other exit code (128 for "not a repository", and so on) propagates. */
150
+ async function readConfig(key, options) {
151
+ try {
152
+ const value = (await git(["config", key], options)).trim();
153
+ return value === "" ? void 0 : value;
154
+ } catch (cause) {
155
+ if (cause instanceof GitCommandError && cause.exitCode === 1) return;
156
+ throw cause;
157
+ }
158
+ }
159
+ /** Commits exactly the given paths (already written to disk) with an explicit identity, so the bump commit does not depend on whatever ambient git configuration the CI runner happens to have. */
160
+ async function commitFiles(files, message, options) {
161
+ await git([
162
+ "add",
163
+ "--",
164
+ ...files
165
+ ], options);
166
+ await git([
167
+ "-c",
168
+ `user.name=${options.identity.name}`,
169
+ "-c",
170
+ `user.email=${options.identity.email}`,
171
+ "commit",
172
+ "-m",
173
+ message,
174
+ "--",
175
+ ...files
176
+ ], options);
177
+ }
178
+ /** Pushes the current branch's head to origin by explicit refspec. Each dependency bump is pushed the moment it is committed -- the same discipline semantic-release applies to its own release commits -- so an interrupted run never leaves local commits that exist nowhere else. */
179
+ async function pushHead(options) {
180
+ await git([
181
+ "push",
182
+ "origin",
183
+ `HEAD:${await currentBranch(options)}`
184
+ ], options);
185
+ }
186
+ function toGitCommandError(args, cwd, cause) {
187
+ const exitCode = cause instanceof Error && "code" in cause && typeof cause.code === "number" ? cause.code : void 0;
188
+ const stderr = cause instanceof Error && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr.trim() : "";
189
+ return new GitCommandError(args, cwd, exitCode, stderr !== "" ? stderr : cause instanceof Error ? cause.message : String(cause));
190
+ }
191
+ //#endregion
192
+ //#region src/json.ts
193
+ function isJsonObject(value) {
194
+ return typeof value === "object" && value !== null && !Array.isArray(value);
195
+ }
196
+ function isStringRecord(value) {
197
+ return isJsonObject(value) && Object.values(value).every((entry) => typeof entry === "string");
198
+ }
199
+ function isStringArray(value) {
200
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
201
+ }
202
+ /**
203
+ * The indentation of the first indented line in a JSON document, so a rewritten manifest keeps the formatting the repository already uses instead of being reflowed to whatever `JSON.stringify` defaults to. A file with no indented line at all (`{}` on one line) has no evidence either way, in which case two spaces -- npm's own default when it writes a `package.json` -- is the closest thing to a neutral choice.
204
+ */
205
+ const FIRST_INDENTED_LINE = /^[ \t]+(?=")/m;
206
+ const NPM_DEFAULT_INDENT = " ";
207
+ function detectIndent(text) {
208
+ const match = FIRST_INDENTED_LINE.exec(text);
209
+ return match === null ? NPM_DEFAULT_INDENT : match[0];
210
+ }
211
+ /** Serialises a JSON document back to text with the indentation and trailing-newline convention of the text it was read from, so rewriting one dependency range produces a one-line diff rather than a whole-file reformat. */
212
+ function stringifyJsonLike(value, originalText) {
213
+ const serialised = JSON.stringify(value, null, detectIndent(originalText));
214
+ return originalText.endsWith("\n") ? `${serialised}\n` : serialised;
215
+ }
216
+ //#endregion
217
+ //#region src/manifest.ts
218
+ const DEPENDENCY_FIELDS = [
219
+ "dependencies",
220
+ "devDependencies",
221
+ "peerDependencies",
222
+ "optionalDependencies"
223
+ ];
224
+ async function readManifest(path) {
225
+ const text = await (0, node_fs_promises.readFile)(path, "utf8");
226
+ const parsed = JSON.parse(text);
227
+ if (!isJsonObject(parsed)) throw new WorkspaceDiscoveryError(`${path} does not contain a JSON object.`);
228
+ const { name, version } = parsed;
229
+ if (typeof name !== "string" || name.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "name". Every workspace package needs a name: releases are ordered, tagged, and matched to dependents by it.`);
230
+ const validity = (0, validate_npm_package_name.default)(name);
231
+ if (!validity.validForOldPackages) throw new WorkspaceDiscoveryError(`${path} has an invalid "name" ("${name}"): ${(validity.errors ?? []).join("; ")}`);
232
+ if (typeof version !== "string" || version.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "version".`);
233
+ const dependencies = /* @__PURE__ */ new Map();
234
+ for (const field of DEPENDENCY_FIELDS) {
235
+ const declared = parsed[field];
236
+ if (declared === void 0) continue;
237
+ if (!isStringRecord(declared)) throw new WorkspaceDiscoveryError(`${path} has a "${field}" field that is not an object of name-to-range strings.`);
238
+ dependencies.set(field, new Map(Object.entries(declared)));
239
+ }
240
+ return {
241
+ name,
242
+ version,
243
+ dependencies
244
+ };
245
+ }
246
+ /**
247
+ * Rewrites one dependency range in a manifest on disk.
248
+ *
249
+ * Deliberately re-reads the file rather than editing a copy held from discovery time: by the time a cross-package bump is applied, semantic-release's own `@semantic-release/npm` prepare step may already have rewritten `version` in this same file for an earlier package in the run. Writing back a manifest parsed before that would silently revert it.
250
+ */
251
+ async function writeDependencyRange(path, field, dependency, range) {
252
+ const text = await (0, node_fs_promises.readFile)(path, "utf8");
253
+ const parsed = JSON.parse(text);
254
+ if (!isJsonObject(parsed)) throw new WorkspaceDiscoveryError(`${path} does not contain a JSON object.`);
255
+ const declared = parsed[field];
256
+ if (!isStringRecord(declared)) throw new WorkspaceDiscoveryError(`${path} no longer has a "${field}" object holding "${dependency}".`);
257
+ declared[dependency] = range;
258
+ await (0, node_fs_promises.writeFile)(path, stringifyJsonLike(parsed, text), "utf8");
259
+ }
260
+ //#endregion
261
+ //#region src/workspace.ts
262
+ /** The one filename pnpm recognises as a workspace definition. */
263
+ const WORKSPACE_MANIFEST = "pnpm-workspace.yaml";
264
+ /** Never treat an installed dependency's own manifest as a workspace package, however permissive the configured globs are. pnpm applies the same exclusion. */
265
+ const INSTALLED_PACKAGES = "**/node_modules/**";
266
+ /**
267
+ * Reads `pnpm-workspace.yaml` and every package manifest its globs match, producing the input to both the dependency graph and the per-package release runs.
268
+ *
269
+ * Nothing here knows anything about a particular repository's layout: the globs come from the workspace file, and the package names, versions, and dependency ranges come from the manifests those globs match. Pointing this at any pnpm workspace is the entire configuration.
270
+ */
271
+ async function discoverWorkspace(root) {
272
+ const workspaceRoot = (0, node_path.resolve)(root);
273
+ const patterns = await readWorkspacePatterns(workspaceRoot);
274
+ const repoPrefix = await resolveRepoPrefix(workspaceRoot);
275
+ const positive = patterns.filter((pattern) => !pattern.startsWith("!"));
276
+ const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
277
+ const manifestPaths = await (0, tinyglobby.glob)(positive.map(toManifestPattern), {
278
+ cwd: workspaceRoot,
279
+ absolute: true,
280
+ expandDirectories: false,
281
+ ignore: [...negative.flatMap((pattern) => [toManifestPattern(pattern), `${trimTrailingSlashes(pattern)}/**`]), INSTALLED_PACKAGES]
282
+ });
283
+ const packages = [];
284
+ const byName = /* @__PURE__ */ new Map();
285
+ for (const manifestPath of [...manifestPaths].sort()) {
286
+ const directory = (0, node_path.dirname)(manifestPath);
287
+ const relativeDirectory = toPosix((0, node_path.relative)(workspaceRoot, directory));
288
+ if (relativeDirectory === "") throw new WorkspaceDiscoveryError(`${WORKSPACE_MANIFEST} matches the workspace root itself. A package at the root cannot be scoped to its own commits, because every commit in the repository touches it; move it into a subdirectory or exclude it from the "packages" globs.`);
289
+ const repoRelativeDirectory = repoPrefix === "" ? relativeDirectory : `${repoPrefix}${relativeDirectory}`;
290
+ const manifest = await readManifest(manifestPath);
291
+ const existing = byName.get(manifest.name);
292
+ if (existing !== void 0) throw new WorkspaceDiscoveryError(`Two workspace packages are both named "${manifest.name}": ${existing} and ${relativeDirectory}. Releases are matched to dependents by name, so names must be unique.`);
293
+ byName.set(manifest.name, relativeDirectory);
294
+ packages.push({
295
+ name: manifest.name,
296
+ version: manifest.version,
297
+ directory,
298
+ relativeDirectory,
299
+ repoRelativeDirectory,
300
+ manifestPath,
301
+ dependencies: manifest.dependencies
302
+ });
303
+ }
304
+ if (packages.length === 0) throw new WorkspaceDiscoveryError(`No packages matched the "packages" globs in ${(0, node_path.resolve)(workspaceRoot, WORKSPACE_MANIFEST)}.`);
305
+ return {
306
+ root: workspaceRoot,
307
+ packages
308
+ };
309
+ }
310
+ /**
311
+ * Resolves the workspace root's own path relative to the git repository's toplevel, via `git rev-parse --show-prefix` -- for example `''` when `pnpm-workspace.yaml` sits at the repository root, or `'monorepo/'` (always POSIX, always either empty or trailing-slash-terminated, per git's own contract for this flag) when the workspace is nested a level below it. `git log --name-only` always reports changed paths relative to the repository's toplevel regardless of the `cwd` a command runs from, so path-scoped commit filtering has to compare against paths built on this prefix, not against paths relative to `pnpm-workspace.yaml`'s own directory alone -- the two differ whenever the workspace is not itself the git toplevel, and comparing against the wrong base makes every path comparison fail silently (see `repoRelativeDirectory`).
312
+ */
313
+ async function resolveRepoPrefix(workspaceRoot) {
314
+ return (await git(["rev-parse", "--show-prefix"], { cwd: workspaceRoot }).catch((cause) => {
315
+ throw new WorkspaceDiscoveryError(`Cannot resolve the git repository toplevel for ${workspaceRoot}: ${cause instanceof Error ? cause.message : String(cause)}. The workspace must sit inside a git repository, because commit analysis is scoped to each package's directory relative to the repository root.`);
316
+ })).trim();
317
+ }
318
+ async function readWorkspacePatterns(workspaceRoot) {
319
+ const path = (0, node_path.resolve)(workspaceRoot, WORKSPACE_MANIFEST);
320
+ const text = await (0, node_fs_promises.readFile)(path, "utf8").catch((cause) => {
321
+ throw new WorkspaceDiscoveryError(`Cannot read ${path}: ${cause instanceof Error ? cause.message : String(cause)}`);
322
+ });
323
+ const parsed = (0, yaml.parse)(text);
324
+ if (!isJsonObject(parsed)) throw new WorkspaceDiscoveryError(`${path} does not contain a YAML mapping.`);
325
+ const { packages } = parsed;
326
+ if (!isStringArray(packages) || packages.length === 0) throw new WorkspaceDiscoveryError(`${path} has no "packages" globs, so it defines no packages to release.`);
327
+ return packages;
328
+ }
329
+ /** pnpm's globs match package *directories*; the glob run here matches the manifest inside each of them, which is both what we need to read and the only reliable evidence a matched directory is a package at all. */
330
+ function toManifestPattern(pattern) {
331
+ return `${trimTrailingSlashes(pattern)}/package.json`;
332
+ }
333
+ const TRAILING_SLASHES = /\/+$/;
334
+ function trimTrailingSlashes(pattern) {
335
+ return pattern.replace(TRAILING_SLASHES, "");
336
+ }
337
+ function toPosix(path) {
338
+ return node_path.sep === "/" ? path : path.split(node_path.sep).join("/");
339
+ }
340
+ //#endregion
341
+ //#region src/graph.ts
342
+ /**
343
+ * Builds the inter-package dependency graph from the manifests alone.
344
+ *
345
+ * A dependency on a package outside the workspace is not an edge: it neither constrains the release order nor gets rewritten when something here releases. A package that names itself becomes a self-edge, which `topologicalOrder` then reports as the one-package cycle it is, rather than being quietly dropped.
346
+ */
347
+ function buildDependencyGraph(packages) {
348
+ const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
349
+ const dependencies = new Map(packages.map((pkg) => [pkg.name, []]));
350
+ const dependents = new Map(packages.map((pkg) => [pkg.name, []]));
351
+ for (const pkg of packages) {
352
+ const outgoing = dependencies.get(pkg.name);
353
+ if (outgoing === void 0) continue;
354
+ for (const [field, declared] of pkg.dependencies) for (const [name, range] of declared) {
355
+ const incoming = dependents.get(name);
356
+ if (incoming === void 0) continue;
357
+ const edge = {
358
+ dependent: pkg.name,
359
+ dependency: name,
360
+ field,
361
+ range
362
+ };
363
+ outgoing.push(edge);
364
+ incoming.push(edge);
365
+ }
366
+ }
367
+ return {
368
+ packages: byName,
369
+ dependencies,
370
+ dependents
371
+ };
372
+ }
373
+ /**
374
+ * Orders packages so every package appears after every workspace sibling it depends on, using Kahn's algorithm.
375
+ *
376
+ * Packages whose dependencies have all been placed are taken in name order, so the same workspace always produces the same order -- a release run that reorders itself between CI runs is impossible to reason about when something goes wrong halfway through.
377
+ *
378
+ * A cycle has no valid order at all, so it throws rather than picking one of the wrong answers. In a release context an arbitrary order is worse than a failure: it would publish a package whose sibling dependency range points at a version that does not exist yet.
379
+ */
380
+ function topologicalOrder(graph) {
381
+ const pending = /* @__PURE__ */ new Map();
382
+ for (const [name, edges] of graph.dependencies) pending.set(name, new Set(edges.map((edge) => edge.dependency)));
383
+ const ordered = [];
384
+ for (;;) {
385
+ const ready = [...pending].filter(([, unplaced]) => unplaced.size === 0).map(([name]) => name).sort();
386
+ if (ready.length === 0) break;
387
+ for (const name of ready) {
388
+ pending.delete(name);
389
+ ordered.push(name);
390
+ }
391
+ for (const unplaced of pending.values()) for (const name of ready) unplaced.delete(name);
392
+ }
393
+ if (pending.size > 0) throw new DependencyCycleError(findCycle(graph, new Set(pending.keys())));
394
+ return ordered;
395
+ }
396
+ /**
397
+ * Walks dependency edges between the packages Kahn's algorithm could not place, until it revisits one, so the error can name a concrete loop rather than just a set of packages. Every unplaced package is unplaced precisely because at least one of its own dependencies is too, so the walk always reaches a repeat.
398
+ */
399
+ function findCycle(graph, unplaced) {
400
+ const path = [];
401
+ const onPath = /* @__PURE__ */ new Set();
402
+ let current = firstUnplacedDependency(void 0, graph, unplaced);
403
+ while (current !== void 0 && !onPath.has(current)) {
404
+ path.push(current);
405
+ onPath.add(current);
406
+ current = firstUnplacedDependency(current, graph, unplaced);
407
+ }
408
+ return current === void 0 ? path : [...path.slice(path.indexOf(current)), current];
409
+ }
410
+ /** With no package given, the alphabetically first unplaced package (the walk's starting point); otherwise that package's alphabetically first still-unplaced dependency. Sorting keeps the reported cycle stable across runs. */
411
+ function firstUnplacedDependency(name, graph, unplaced) {
412
+ if (name === void 0) return [...unplaced].sort()[0];
413
+ return (graph.dependencies.get(name) ?? []).map((edge) => edge.dependency).filter((dependency) => unplaced.has(dependency)).sort()[0];
414
+ }
415
+ //#endregion
416
+ //#region src/version-range.ts
417
+ const WORKSPACE_PROTOCOL = "workspace:";
418
+ const CATALOG_PROTOCOL = "catalog:";
419
+ const NPM_ALIAS_PROTOCOL = "npm:";
420
+ /** The `workspace:` suffixes pnpm resolves against the sibling's version at pack time rather than against anything written in the manifest. */
421
+ const PUBLISH_RESOLVED_WORKSPACE_SUFFIXES = [
422
+ "*",
423
+ "^",
424
+ "~"
425
+ ];
426
+ /** Ranges that pin nothing, so a sibling's new version cannot change what they mean. */
427
+ const WILDCARD_RANGES = [
428
+ "",
429
+ "*",
430
+ "x",
431
+ "X",
432
+ "latest"
433
+ ];
434
+ /**
435
+ * A single comparator whose version can be replaced in place without changing the comparator's intent. `<` and `<=` are deliberately absent: rewriting `<2.0.0` to `<1.4.0` narrows an upper bound to the very version being released, which is never what the author meant, so such a range is rejected rather than mangled.
436
+ */
437
+ const REWRITABLE_COMPARATOR = /^(\^|~|>=|=)?(\d+\.\d+\.\d+(?:-[\dA-Za-z.-]+)?(?:\+[\dA-Za-z.-]+)?)$/;
438
+ /**
439
+ * Classifies a dependency range's shape, throwing `UnsupportedDependencyRangeError` for anything this tool cannot rewrite with confidence: a compound range (`>=1.0.0 <2.0.0`), a union (`1.x || 2.x`), a `catalog:` reference whose real version lives in `pnpm-workspace.yaml`, an `npm:` alias, a git or tarball URL. Guessing at those would either corrupt the range or silently leave it pointing at a version that no longer exists in the workspace, and a stale published range is exactly the divergence this tool exists to prevent.
440
+ *
441
+ * This never needs the version a sibling is releasing: every case above depends only on the shape of `current` itself, which is what lets `releaseWorkspace` validate every workspace dependency edge up front, before any package has published anything, rather than discovering an unsupported range only when the first dependency it names happens to release.
442
+ */
443
+ function classifyDependencyRange(current) {
444
+ const range = current.trim();
445
+ if (range.startsWith(WORKSPACE_PROTOCOL)) {
446
+ const suffix = range.slice(10);
447
+ if (PUBLISH_RESOLVED_WORKSPACE_SUFFIXES.includes(suffix)) return { kind: "resolved-at-publish" };
448
+ const inner = classifyDependencyRange(suffix);
449
+ if (inner.kind !== "rewritable") throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": only "workspace:*", "workspace:^", "workspace:~", and "workspace:" followed by a single concrete version range are supported.`);
450
+ return {
451
+ kind: "rewritable",
452
+ workspacePrefixed: true,
453
+ comparator: inner.comparator
454
+ };
455
+ }
456
+ if (range.startsWith(CATALOG_PROTOCOL)) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": the version of a "catalog:" dependency lives in pnpm-workspace.yaml, not in the package manifest, so bumping it here would leave the catalog entry stale. Depend on the sibling directly (for example "workspace:^") instead.`);
457
+ if (range.startsWith(NPM_ALIAS_PROTOCOL)) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": an "npm:" alias points at a differently-named package, so the version released in this workspace is not necessarily the version this range refers to.`);
458
+ if (WILDCARD_RANGES.includes(range)) return { kind: "wildcard" };
459
+ const match = REWRITABLE_COMPARATOR.exec(range);
460
+ if (match === null) throw new UnsupportedDependencyRangeError(`Cannot bump the workspace dependency range "${current}": only a single "^", "~", ">=", "=", or bare version comparator can be rewritten in place.`);
461
+ return {
462
+ kind: "rewritable",
463
+ workspacePrefixed: false,
464
+ comparator: match[1] ?? ""
465
+ };
466
+ }
467
+ /**
468
+ * Computes what a dependency range on a workspace sibling becomes once that sibling releases `version`, by classifying the range's shape and then, for a rewritable shape, substituting `version` in place of the version it currently names.
469
+ */
470
+ function updateDependencyRange(current, version) {
471
+ const shape = classifyDependencyRange(current);
472
+ if (shape.kind !== "rewritable") return shape;
473
+ const rewritten = `${shape.comparator}${version}`;
474
+ return {
475
+ kind: "rewritten",
476
+ range: shape.workspacePrefixed ? `${WORKSPACE_PROTOCOL}${rewritten}` : rewritten
477
+ };
478
+ }
479
+ //#endregion
480
+ //#region src/dependency-bump-commit.ts
481
+ /**
482
+ * The commit message format for a cross-package dependency-range bump, shared between the code that writes it (`bumpDependents` in `release.ts`) and the code that reads it back (`createScopedPlugins` in `plugins.ts`).
483
+ *
484
+ * The subject line alone (`chore(deps): bump @scope/a to ^1.1.0 in @scope/b [skip ci]`) is for humans reading `git log`. Recovering the forced-patch decision from a commit already sitting in history -- rather than only from the in-memory record a single run builds as it goes -- needs a machine-parseable form as well, because a run that starts after a previous run already committed and pushed the bump (whether that previous run crashed immediately afterwards, or simply finished days ago) has no in-memory record at all: the only place the fact "this dependency range changed because a sibling released" is stated is the repository itself. The trailer below is that statement.
485
+ */
486
+ const TRAILER_DEPENDENCY = "Bumped-Workspace-Dependency";
487
+ const TRAILER_VERSION = "Bumped-Workspace-Dependency-Version";
488
+ const TRAILER_RANGE = "Bumped-Workspace-Dependency-Range";
489
+ /** Builds the full commit message (subject and trailer) for one dependency-range bump. */
490
+ function formatDependencyBumpMessage(info) {
491
+ return `${`chore(deps): bump ${info.dependency} to ${info.range} in ${info.dependent} [skip ci]`}\n\n${[
492
+ `${TRAILER_DEPENDENCY}: ${info.dependency}`,
493
+ `${TRAILER_VERSION}: ${info.version}`,
494
+ `${TRAILER_RANGE}: ${info.range}`
495
+ ].join("\n")}`;
496
+ }
497
+ /**
498
+ * Recovers the dependency bump a `formatDependencyBumpMessage` commit recorded, from its full git message (subject and body), or `undefined` if the message carries no such trailer. All three lines must be present for the commit to be treated as a bump commit at all -- a message missing even one is left alone rather than partially trusted.
499
+ */
500
+ function parseDependencyBumpTrailer(message) {
501
+ const dependency = matchTrailerLine(message, TRAILER_DEPENDENCY);
502
+ const version = matchTrailerLine(message, TRAILER_VERSION);
503
+ const range = matchTrailerLine(message, TRAILER_RANGE);
504
+ if (dependency === void 0 || version === void 0 || range === void 0) return;
505
+ return {
506
+ dependency,
507
+ version,
508
+ range
509
+ };
510
+ }
511
+ function matchTrailerLine(message, key) {
512
+ const prefix = `${key}: `;
513
+ const line = message.split("\n").find((candidate) => candidate.startsWith(prefix));
514
+ return line === void 0 ? void 0 : line.slice(prefix.length).trim();
515
+ }
516
+ //#endregion
517
+ //#region src/plugins.ts
518
+ /** The standard publish pipeline this orchestrator coordinates when a workspace configures none of its own. Every entry reuses the corresponding official plugin -- the orchestrator scopes and sequences them per package, it does not reimplement npm publishing, GitHub release creation, or changelog writing. */
519
+ const DEFAULT_PUBLISH_PLUGINS = [
520
+ "@semantic-release/changelog",
521
+ "@semantic-release/npm",
522
+ "@semantic-release/github",
523
+ ["@semantic-release/git", {
524
+ assets: ["CHANGELOG.md", "package.json"],
525
+ message: "chore(release): ${nextRelease.gitTag} [skip ci]"
526
+ }]
527
+ ];
528
+ const STEP_PLUGINS_THE_ORCHESTRATOR_OWNS = /* @__PURE__ */ new Set(["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator"]);
529
+ /**
530
+ * Builds the per-package `analyzeCommits` and `generateNotes` functions handed to semantic-release as inline plugins.
531
+ *
532
+ * Both apply the same path scoping before delegating to the real @semantic-release/commit-analyzer and @semantic-release/release-notes-generator: the commit list semantic-release already fetched for the release range is filtered down to commits whose `git log --name-only` file list intersects the package's own directory, and only the filtered list reaches the standard plugin. Conventional-commit parsing and changelog formatting stay entirely inside the standard plugins.
533
+ *
534
+ * The `analyzeCommits` wrapper carries one addition beyond filtering: when the standard analyzer finds no releasable commits but a workspace dependency range of the package's has changed, it returns 'patch' anyway. A dependent whose only change is a dependency bump still needs a release for that range to reach the registry. "Has changed" is read from two sources, merged: bumps recorded in memory earlier in the current run (`scope.bumps`), and bumps recorded in the package's own filtered commit history via the trailer `dependency-bump-commit.ts` writes and reads -- the latter is what lets a run that starts after a previous run already committed and pushed the bump (a crash recovery, or simply a later run) reach the same decision, rather than depending on state that existed only inside the process that made the commit.
535
+ */
536
+ function createScopedPlugins(scope) {
537
+ let cached;
538
+ async function commitsForPackage(context) {
539
+ const from = context.lastRelease?.gitHead ?? void 0;
540
+ if (cached?.from !== from) cached = {
541
+ from,
542
+ paths: changedPathsSince(from, { cwd: context.cwd })
543
+ };
544
+ return filterCommitsToDirectory(context.commits, await cached.paths, scope.pkg.repoRelativeDirectory);
545
+ }
546
+ return {
547
+ async analyzeCommits(_pluginConfig, context) {
548
+ const commits = await commitsForPackage(context);
549
+ const type = await (0, _semantic_release_commit_analyzer.analyzeCommits)(scope.analyzeCommitsConfig, {
550
+ ...context,
551
+ commits
552
+ });
553
+ if (type) return type;
554
+ const bumps = mergeDependencyBumps(scope.bumps.bumpsFor(scope.pkg.name), commits);
555
+ if (bumps.length === 0) return false;
556
+ context.logger.log(`No releasable commits under ${scope.pkg.relativeDirectory}, but ${bumps.length === 1 ? "a workspace dependency range changed" : `${bumps.length} workspace dependency ranges changed`}; forcing a patch release.`);
557
+ return "patch";
558
+ },
559
+ async generateNotes(_pluginConfig, context) {
560
+ const commits = await commitsForPackage(context);
561
+ const notes = await (0, _semantic_release_release_notes_generator.generateNotes)(scope.generateNotesConfig, {
562
+ ...context,
563
+ commits
564
+ });
565
+ const bumps = mergeDependencyBumps(scope.bumps.bumpsFor(scope.pkg.name), commits);
566
+ if (bumps.length === 0) return notes;
567
+ const section = [
568
+ "### Dependencies",
569
+ "",
570
+ ...bumps.map((bump) => describeDependencyBump(bump))
571
+ ].join("\n");
572
+ return notes ? `${notes}\n\n${section}` : section;
573
+ }
574
+ };
575
+ }
576
+ /**
577
+ * Combines the bumps recorded in memory earlier in the current run with bumps recovered from the package's own filtered commit history (a bump commit from this run, already visible because it touches the package's own directory, or one left over from a previous run), de-duplicated by dependency name. The in-memory entry wins on overlap: it carries the manifest field and dependent name a `resolved-at-publish` bump has no commit to recover from at all.
578
+ */
579
+ function mergeDependencyBumps(runtimeBumps, commits) {
580
+ const byDependency = /* @__PURE__ */ new Map();
581
+ for (const commit of commits) {
582
+ const parsed = parseDependencyBumpTrailer(commit.message);
583
+ if (parsed !== void 0) byDependency.set(parsed.dependency, {
584
+ ...parsed,
585
+ kind: "rewritten"
586
+ });
587
+ }
588
+ for (const bump of runtimeBumps) byDependency.set(bump.dependency, bump);
589
+ return [...byDependency.values()];
590
+ }
591
+ function describeDependencyBump(bump) {
592
+ return bump.kind === "rewritten" ? `- Updated ${bump.dependency} to ${bump.range}` : `- Updated ${bump.dependency} to ${bump.version} (declared as \`${bump.range}\`, resolved by pnpm at publish time)`;
593
+ }
594
+ /**
595
+ * Keeps a commit for the package when any path it changed lies under the package's directory. The trailing-slash prefix comparison stops `packages/a` from matching `packages/abc/x`.
596
+ *
597
+ * A commit missing from the changed-paths map is kept rather than dropped: it is inside the package's release range (semantic-release put it there), so a failure to parse its file list must not silently swallow a release. Absent evidence errs towards publishing, which is the visible direction for a release tool.
598
+ */
599
+ function filterCommitsToDirectory(commits, changedPaths, directory) {
600
+ const prefix = `${directory}/`;
601
+ return commits.filter((commit) => {
602
+ const paths = changedPaths.get(commit.hash);
603
+ if (paths === void 0) return true;
604
+ return [...paths].some((path) => path === directory || path.startsWith(prefix));
605
+ });
606
+ }
607
+ function resolvePublishPlugins(specs, workspaceRoot, options) {
608
+ const requireFromTool = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
609
+ const requireFromWorkspace = (0, node_module.createRequire)((0, node_path.resolve)(workspaceRoot, "package.json"));
610
+ const resolved = [];
611
+ let hasGitPlugin = false;
612
+ for (const spec of specs) {
613
+ const [name, config] = parsePublishPluginSpec(spec);
614
+ if (STEP_PLUGINS_THE_ORCHESTRATOR_OWNS.has(name)) throw new ReleaseConfigurationError(`"${name}" is listed as a publish plugin, but ${packageName} always provides the ${name === "@semantic-release/commit-analyzer" ? "analyzeCommits" : "generateNotes"} step itself, wrapped around that plugin. Passing it here would make its configuration a silent no-op; set that configuration on the orchestrator's analyzeCommits/generateNotes options instead.`);
615
+ if (name === "@semantic-release/git") hasGitPlugin = true;
616
+ const entry = [resolvePluginModule(name, requireFromTool, requireFromWorkspace), config];
617
+ resolved.push(entry);
618
+ }
619
+ if (options.requireGitPlugin && !hasGitPlugin) throw new ReleaseConfigurationError(`The publish plugin list does not include @semantic-release/git. Without it, nothing commits each released package's manifest and changelog back to the branch, so the repository would drift out of agreement with the published versions -- the exact divergence this tool exists to prevent. (Dry runs are exempt.)`);
620
+ return resolved;
621
+ }
622
+ /**
623
+ * Resolves a plugin module name to an absolute file path, first from this tool's own module context (its peer dependencies, which every workspace installing the orchestrator must provide) and then from the workspace root (a workspace's own plugin dependencies, such as a custom changelog plugin). Both bases are named in the error when neither can resolve the name.
624
+ */
625
+ function resolvePluginModule(name, requireFromTool, requireFromWorkspace) {
626
+ const attempts = [];
627
+ for (const [label, requirer] of [["this tool", requireFromTool], ["the workspace root", requireFromWorkspace]]) try {
628
+ return requirer.resolve(name);
629
+ } catch (cause) {
630
+ attempts.push(`${label}: ${cause instanceof Error ? cause.message : String(cause)}`);
631
+ }
632
+ throw new ReleaseConfigurationError(`Cannot resolve the publish plugin "${name}". Tried resolving it from ${attempts.join("; and from ")}.`);
633
+ }
634
+ function parsePublishPluginSpec(spec) {
635
+ if (typeof spec === "string") return [spec, {}];
636
+ const [name, config] = spec;
637
+ return [name, config ?? {}];
638
+ }
639
+ //#endregion
640
+ //#region src/release.ts
641
+ /**
642
+ * Releases every package in a pnpm workspace with independent versions, in dependency order.
643
+ *
644
+ * For each package, in topological order: run semantic-release's programmatic API with `cwd` scoped to the package directory, a `name@version` tag format to keep each package's tags distinct in the one shared tag namespace, and inline `analyzeCommits`/`generateNotes` plugins that filter the release range's commits down to the package's own directory before delegating to the standard plugins. When a package releases, every workspace package that depends on it and has not run yet gets its dependency range rewritten in its manifest and committed immediately -- before its own turn, so its commit analysis and its published manifest both see the new range.
645
+ */
646
+ async function releaseWorkspace(options = {}) {
647
+ const root = (0, node_path.resolve)(options.root ?? process.cwd());
648
+ const log = options.log ?? console.log;
649
+ const dryRun = options.dryRun === true;
650
+ const env = options.env ?? process.env;
651
+ const workspace = await discoverWorkspace(root);
652
+ const graph = buildDependencyGraph(workspace.packages);
653
+ validateDependencyRangeShapes(graph);
654
+ const order = topologicalOrder(graph);
655
+ log(`${packageName}: ${order.length} packages in release order: ${order.join(" -> ")}`);
656
+ const publishPlugins = resolvePublishPlugins(options.plugins ?? DEFAULT_PUBLISH_PLUGINS, workspace.root, { requireGitPlugin: !dryRun });
657
+ const analyzeCommitsConfig = options.analyzeCommits ?? {};
658
+ const generateNotesConfig = options.generateNotes ?? {};
659
+ const pendingBumps = /* @__PURE__ */ new Map();
660
+ let identity;
661
+ const outcomes = [];
662
+ for (const name of order) {
663
+ const pkg = mustGet(graph.packages, name, "package");
664
+ const bumpsForThisPackage = pendingBumps.get(name) ?? [];
665
+ log(`Releasing ${name} from ${pkg.relativeDirectory}${bumpsForThisPackage.length > 0 ? ` (dependency ranges already bumped: ${bumpsForThisPackage.map((bump) => bump.dependency).join(", ")})` : ""}`);
666
+ const result = await runPackageRelease(pkg, {
667
+ publishPlugins,
668
+ analyzeCommitsConfig,
669
+ generateNotesConfig,
670
+ bumpsForThisPackage,
671
+ dryRun,
672
+ env,
673
+ branches: options.branches
674
+ });
675
+ const nextRelease = result === false ? void 0 : result.nextRelease;
676
+ outcomes.push({
677
+ name,
678
+ directory: pkg.directory,
679
+ released: nextRelease !== void 0,
680
+ version: nextRelease?.version,
681
+ gitTag: nextRelease?.gitTag,
682
+ type: nextRelease?.type,
683
+ dependencyBumps: bumpsForThisPackage
684
+ });
685
+ pendingBumps.delete(name);
686
+ if (nextRelease === void 0) {
687
+ log(`${name}: no release`);
688
+ continue;
689
+ }
690
+ log(`${name}: released ${nextRelease.gitTag}`);
691
+ identity ??= await resolveCommitIdentity({ cwd: workspace.root });
692
+ const bumps = await bumpDependents(pkg, nextRelease.version, graph, {
693
+ workspace,
694
+ dryRun,
695
+ identity,
696
+ log
697
+ });
698
+ for (const bump of bumps) {
699
+ const forDependent = pendingBumps.get(bump.dependent) ?? [];
700
+ forDependent.push(bump);
701
+ pendingBumps.set(bump.dependent, forDependent);
702
+ }
703
+ }
704
+ return {
705
+ order,
706
+ packages: outcomes
707
+ };
708
+ }
709
+ /**
710
+ * Checks every workspace dependency edge's range shape before anything releases, so an `UnsupportedDependencyRangeError` stops the run before the first publish rather than after some sibling has already been published, tagged, committed, and pushed. The shape a range supports depends only on the range text itself (see `classifyDependencyRange`), never on which version a sibling ends up releasing, so this can run once up front for the whole graph instead of only being discovered edge by edge as each dependency happens to release.
711
+ */
712
+ function validateDependencyRangeShapes(graph) {
713
+ for (const edges of graph.dependencies.values()) for (const edge of edges) classifyDependencyRange(edge.range);
714
+ }
715
+ async function runPackageRelease(pkg, options) {
716
+ const scoped = createScopedPlugins({
717
+ pkg,
718
+ analyzeCommitsConfig: options.analyzeCommitsConfig,
719
+ generateNotesConfig: options.generateNotesConfig,
720
+ bumps: { bumpsFor: () => options.bumpsForThisPackage }
721
+ });
722
+ const semanticReleaseOptions = {
723
+ tagFormat: `${pkg.name}@\${version}`,
724
+ plugins: options.publishPlugins,
725
+ analyzeCommits: scoped.analyzeCommits,
726
+ generateNotes: scoped.generateNotes
727
+ };
728
+ if (options.dryRun) semanticReleaseOptions.dryRun = true;
729
+ if (options.branches !== void 0) semanticReleaseOptions.branches = options.branches;
730
+ try {
731
+ return await (0, semantic_release.default)(semanticReleaseOptions, {
732
+ cwd: pkg.directory,
733
+ env: { ...options.env }
734
+ });
735
+ } catch (cause) {
736
+ throw new WorkspaceReleaseError(`Release of ${pkg.name} failed: ${cause instanceof Error ? cause.message : String(cause)}`);
737
+ }
738
+ }
739
+ /**
740
+ * Rewrites the released package's range in every dependent's manifest, immediately after the release and before any dependent's own turn.
741
+ *
742
+ * The bump is committed (and pushed) right away rather than staged, because a dependent's semantic-release run analyses git history, not the working tree: an uncommitted bump would be invisible to its commit analysis, and would be stranded uncommitted if the dependent then released nothing -- a manifest that names versions the registry has never seen. Committing immediately means the dependent's own run sees the bump commit (it touches only the dependent's directory, so it passes that dependent's path filter), the forced-patch logic in the scoped analyzer covers the case where that bump is the dependent's only change, and a run interrupted partway leaves the remote describing exactly what was published. Pushing immediately mirrors what semantic-release itself does with release commits.
743
+ */
744
+ async function bumpDependents(released, version, graph, options) {
745
+ const applied = [];
746
+ const dependents = graph.dependents.get(released.name);
747
+ if (dependents === void 0) return applied;
748
+ for (const edge of dependents) {
749
+ const update = updateDependencyRange(edge.range, version);
750
+ if (update.kind === "wildcard") continue;
751
+ const dependent = mustGet(graph.packages, edge.dependent, "package");
752
+ if (update.kind === "rewritten") {
753
+ if (!options.dryRun) {
754
+ await writeDependencyRange(dependent.manifestPath, edge.field, released.name, update.range);
755
+ const message = formatDependencyBumpMessage({
756
+ dependency: released.name,
757
+ version,
758
+ range: update.range,
759
+ dependent: edge.dependent
760
+ });
761
+ await commitFiles([`${dependent.relativeDirectory}/package.json`], message, {
762
+ cwd: options.workspace.root,
763
+ identity: options.identity
764
+ });
765
+ await pushHead({ cwd: options.workspace.root });
766
+ options.log(`Bumped ${released.name} to ${update.range} in ${edge.dependent}, committed and pushed`);
767
+ } else options.log(`Would bump ${released.name} to ${update.range} in ${edge.dependent} (${edge.field})`);
768
+ } else options.log(`${edge.dependent} declares ${released.name} as ${edge.range}; the manifest needs no edit, pnpm re-resolves it to ${version} at publish time`);
769
+ applied.push({
770
+ dependent: edge.dependent,
771
+ dependency: released.name,
772
+ field: edge.field,
773
+ version,
774
+ range: update.kind === "rewritten" ? update.range : edge.range,
775
+ kind: update.kind
776
+ });
777
+ }
778
+ return applied;
779
+ }
780
+ function mustGet(map, key, what) {
781
+ const value = map.get(key);
782
+ if (value === void 0) throw new WorkspaceReleaseError(`Internal error: ${what} "${key}" disappeared from the dependency graph mid-run.`);
783
+ return value;
784
+ }
785
+ //#endregion
786
+ exports.DEFAULT_PUBLISH_PLUGINS = DEFAULT_PUBLISH_PLUGINS;
787
+ exports.DependencyCycleError = DependencyCycleError;
788
+ exports.GitCommandError = GitCommandError;
789
+ exports.ReleaseConfigurationError = ReleaseConfigurationError;
790
+ exports.UnsupportedDependencyRangeError = UnsupportedDependencyRangeError;
791
+ exports.WorkspaceDiscoveryError = WorkspaceDiscoveryError;
792
+ exports.WorkspaceReleaseError = WorkspaceReleaseError;
793
+ exports.WorkspaceStateError = WorkspaceStateError;
794
+ exports.buildDependencyGraph = buildDependencyGraph;
795
+ exports.classifyDependencyRange = classifyDependencyRange;
796
+ exports.createScopedPlugins = createScopedPlugins;
797
+ exports.discoverWorkspace = discoverWorkspace;
798
+ exports.filterCommitsToDirectory = filterCommitsToDirectory;
799
+ exports.packageName = packageName;
800
+ exports.readManifest = readManifest;
801
+ exports.releaseWorkspace = releaseWorkspace;
802
+ exports.resolvePublishPlugins = resolvePublishPlugins;
803
+ exports.topologicalOrder = topologicalOrder;
804
+ exports.updateDependencyRange = updateDependencyRange;
805
+ exports.writeDependencyRange = writeDependencyRange;