@savvy-web/silk-effects 1.5.2 → 2.0.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.
@@ -1,6 +1,35 @@
1
1
  import { sortDependencyRows } from "./dependency-table.js";
2
+ import { Option } from "effect";
2
3
 
3
4
  //#region src/changesets/utils/dep-diff.ts
5
+ /**
6
+ * Compute per-workspace-package dependency-table rows from two
7
+ * {@link WorkspaceStateSnapshot}s, resolving `catalog:` / `workspace:`
8
+ * specifiers against each side's own catalogs and package versions BEFORE
9
+ * comparing.
10
+ *
11
+ * @remarks
12
+ * Operates on declared dependencies only (the `dependencies` /
13
+ * `devDependencies` / `peerDependencies` / `optionalDependencies` fields
14
+ * of each workspace's `package.json`). Lockfile-only movements
15
+ * (resolved versions changing while declared ranges stay put) are
16
+ * intentionally excluded — those happen on every `pnpm install` and
17
+ * would generate constant noise.
18
+ *
19
+ * Each side carries its own catalogs and package versions, so a specifier
20
+ * is resolved against the snapshot it belongs to: `catalog:silk` resolves
21
+ * to that ref's `silk` catalog entry, `workspace:*` to that ref's target
22
+ * package version. A row is emitted iff the two RESOLVED values differ (or
23
+ * the dependency was added/removed) — a package that merely adopted a
24
+ * `catalog:` specifier without changing the concrete version produces NO
25
+ * row. When a side cannot resolve a specifier (no matching catalog entry,
26
+ * plain range, etc.) it falls back to the raw specifier string.
27
+ *
28
+ * @see {@link DependencyTableRow} for the row schema
29
+ * @see {@link WorkspaceStateSnapshot} for the input shape
30
+ *
31
+ */
32
+ /** The em-dash sentinel (U+2014) used for added ("from") / removed ("to") cells. */
4
33
  const EM_DASH = "—";
5
34
  const DEP_TYPE_MAP = [
6
35
  ["dependencies", "dependency"],
@@ -8,65 +37,70 @@ const DEP_TYPE_MAP = [
8
37
  ["peerDependencies", "peerDependency"],
9
38
  ["optionalDependencies", "optionalDependency"]
10
39
  ];
11
- function diffOneRecord(before, after, type) {
12
- const rows = [];
13
- const seen = /* @__PURE__ */ new Set();
14
- for (const [name, beforeVersion] of Object.entries(before)) {
15
- seen.add(name);
16
- const afterVersion = after[name];
17
- if (afterVersion === void 0) rows.push({
18
- dependency: name,
19
- type,
20
- action: "removed",
21
- from: beforeVersion,
22
- to: EM_DASH
23
- });
24
- else if (afterVersion !== beforeVersion) rows.push({
25
- dependency: name,
26
- type,
27
- action: "updated",
28
- from: beforeVersion,
29
- to: afterVersion
30
- });
31
- }
32
- for (const [name, afterVersion] of Object.entries(after)) {
33
- if (seen.has(name)) continue;
34
- rows.push({
35
- dependency: name,
36
- type,
37
- action: "added",
38
- from: EM_DASH,
39
- to: afterVersion
40
- });
41
- }
42
- return rows;
43
- }
44
40
  /**
45
- * Diff two workspace snapshots and return per-package dependency-table rows.
41
+ * Resolve a specifier against the snapshot it belongs to, falling back to
42
+ * the raw specifier string when the snapshot cannot resolve it.
43
+ */
44
+ const resolveOrRaw = (snapshot, dep, spec) => Option.getOrElse(snapshot.resolve(dep, spec), () => spec);
45
+ /**
46
+ * Diff two workspace snapshots and return per-package dependency-table rows,
47
+ * comparing already-resolved specifier values per side.
46
48
  *
47
- * @param before - Snapshot at the older ref (typically the merge base). Pass
48
- * `null` for workspace packages that did not exist at the older ref — every
49
- * declared dep is then reported as `"added"`.
49
+ * @param before - Snapshot at the older ref (typically the merge base). A
50
+ * workspace package absent here reports every declared dep as `"added"`.
50
51
  * @param after - Snapshot at the newer ref (typically the working tree).
51
52
  * @returns One {@link WorkspaceDependencyDiff} entry per workspace package
52
- * that has at least one row. Packages with no changes are omitted.
53
+ * that has at least one row. Packages with no resolved-value changes are
54
+ * omitted.
53
55
  *
54
56
  * @public
55
57
  */
56
- function computeWorkspaceDependencyDiffs(beforeSnapshots, afterSnapshots) {
57
- const beforeByName = new Map(beforeSnapshots.map((s) => [s.name, s]));
58
+ function computeWorkspaceDependencyDiffs(before, after) {
58
59
  const result = [];
59
- for (const after of afterSnapshots) {
60
- const before = beforeByName.get(after.name);
60
+ for (const afterPkg of after.packages) {
61
+ const beforePkg = Option.getOrNull(before.package(afterPkg.name));
61
62
  const rows = [];
62
63
  for (const [field, type] of DEP_TYPE_MAP) {
63
- const beforeRecord = before?.[field] ?? {};
64
- const afterRecord = after[field];
65
- rows.push(...diffOneRecord(beforeRecord, afterRecord, type));
64
+ const beforeRecord = beforePkg?.[field] ?? {};
65
+ const afterRecord = afterPkg[field];
66
+ const seen = /* @__PURE__ */ new Set();
67
+ for (const [name, beforeSpec] of Object.entries(beforeRecord)) {
68
+ seen.add(name);
69
+ const from = resolveOrRaw(before, name, beforeSpec);
70
+ const afterSpec = afterRecord[name];
71
+ if (afterSpec === void 0) {
72
+ rows.push({
73
+ dependency: name,
74
+ type,
75
+ action: "removed",
76
+ from,
77
+ to: EM_DASH
78
+ });
79
+ continue;
80
+ }
81
+ const to = resolveOrRaw(after, name, afterSpec);
82
+ if (from !== to) rows.push({
83
+ dependency: name,
84
+ type,
85
+ action: "updated",
86
+ from,
87
+ to
88
+ });
89
+ }
90
+ for (const [name, afterSpec] of Object.entries(afterRecord)) {
91
+ if (seen.has(name)) continue;
92
+ rows.push({
93
+ dependency: name,
94
+ type,
95
+ action: "added",
96
+ from: EM_DASH,
97
+ to: resolveOrRaw(after, name, afterSpec)
98
+ });
99
+ }
66
100
  }
67
101
  if (rows.length > 0) result.push({
68
- package: after.name,
69
- relativePath: after.relativePath,
102
+ package: afterPkg.name,
103
+ relativePath: afterPkg.relativePath,
70
104
  rows: sortDependencyRows(rows)
71
105
  });
72
106
  }
@@ -0,0 +1,51 @@
1
+ import { GitError } from "../errors.js";
2
+ import { Effect } from "effect";
3
+ import { execFileSync } from "node:child_process";
4
+
5
+ //#region src/changesets/utils/git.ts
6
+ /**
7
+ * Git helpers for the changesets deps regen/detect orchestration.
8
+ *
9
+ * @remarks
10
+ * `PointInTimeWorkspace` (from `workspaces-effect`) reads both sides of a
11
+ * dependency diff over `CommandExecutor`. The one git operation it does not
12
+ * cover is resolving the default `--from` ref — the merge-base with the base
13
+ * branch — which stays here as a synchronous `execFileSync` shell-out.
14
+ *
15
+ * @internal
16
+ */
17
+ /**
18
+ * Run `git merge-base <base> HEAD`, returning the SHA. Errors propagate
19
+ * as {@link GitError}.
20
+ *
21
+ * @internal
22
+ */
23
+ function gitMergeBase(cwd, base) {
24
+ return Effect.try({
25
+ try: () => execFileSync("git", [
26
+ "merge-base",
27
+ base,
28
+ "HEAD"
29
+ ], {
30
+ cwd,
31
+ encoding: "utf8",
32
+ stdio: [
33
+ "ignore",
34
+ "pipe",
35
+ "pipe"
36
+ ]
37
+ }).trim(),
38
+ catch: (error) => {
39
+ const stderr = error.stderr;
40
+ const text = typeof stderr === "string" ? stderr : stderr?.toString() ?? "";
41
+ return new GitError({
42
+ command: `git merge-base ${base} HEAD`,
43
+ cwd,
44
+ reason: text.trim() || (error.message ?? String(error))
45
+ });
46
+ }
47
+ });
48
+ }
49
+
50
+ //#endregion
51
+ export { gitMergeBase };
@@ -20,16 +20,28 @@ import { PublishabilityDetector } from "workspaces-effect";
20
20
  * Uses the currently-active {@link SilkPublishability} — wire the
21
21
  * {@link SilkPublishabilityDetectorLive} layer to get silk semantics.
22
22
  *
23
+ * `root` is passed through verbatim to `detector.detect(pkg, root)` for every
24
+ * package — it must be the project root (the directory containing
25
+ * `.changeset/`), NOT the individual package's directory. The vanilla
26
+ * `PublishabilityDetectorLive` and plain `SilkPublishabilityDetectorLive`
27
+ * both ignore this argument, but the ignore/mode-aware
28
+ * `PublishabilityDetectorAdaptiveLive` reads `.changeset/config.json`
29
+ * relative to it, so passing a package subdirectory silently makes every
30
+ * package resolve to "not publishable". Mirrors
31
+ * {@link SilkPublishability.listPublishable}, which takes the same
32
+ * single-root parameter for the same reason.
33
+ *
23
34
  * @param packages - The workspace packages to evaluate
35
+ * @param root - Absolute path to the project root containing `.changeset/`
24
36
  * @returns An Effect yielding a `Set` of publishable package names
25
37
  *
26
38
  * @public
27
39
  */
28
- function listPublishablePackageNames(packages) {
40
+ function listPublishablePackageNames(packages, root) {
29
41
  return Effect.gen(function* () {
30
42
  const detector = yield* PublishabilityDetector;
31
43
  const names = /* @__PURE__ */ new Set();
32
- for (const pkg of packages) if ((yield* detector.detect(pkg, pkg.path)).length > 0) names.add(pkg.name);
44
+ for (const pkg of packages) if ((yield* detector.detect(pkg, root)).length > 0) names.add(pkg.name);
33
45
  return names;
34
46
  });
35
47
  }