@savvy-web/silk 1.3.6 → 1.3.7
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/changesets-changelog.cjs +780 -451
- package/changesets-changelog.d.cts +555 -92
- package/changesets-changelog.d.ts +555 -92
- package/changesets-markdownlint.cjs +780 -451
- package/changesets-markdownlint.d.cts +555 -92
- package/changesets-markdownlint.d.ts +555 -92
- package/package.json +4 -4
- package/packages/silk-effects/dist/dev/pkg/changesets/api/linter.cjs +12 -10
- package/packages/silk-effects/dist/dev/pkg/changesets/api/linter.js +9 -7
- package/packages/silk-effects/dist/dev/pkg/changesets/index.cjs +23 -10
- package/packages/silk-effects/dist/dev/pkg/changesets/index.js +17 -10
- package/packages/silk-effects/dist/dev/pkg/changesets/markdownlint/rules/dependency-table-format.cjs +3 -3
- package/packages/silk-effects/dist/dev/pkg/changesets/markdownlint/rules/dependency-table-format.js +1 -1
- package/packages/silk-effects/dist/dev/pkg/changesets/remark/presets.cjs +1 -1
- package/packages/silk-effects/dist/dev/pkg/changesets/remark/presets.js +1 -1
- package/packages/silk-effects/dist/dev/pkg/changesets/schemas/dependency-table.cjs +12 -1
- package/packages/silk-effects/dist/dev/pkg/changesets/schemas/dependency-table.js +12 -2
- package/packages/silk-effects/dist/dev/pkg/changesets/services/deps-regen.cjs +331 -0
- package/packages/silk-effects/dist/dev/pkg/changesets/services/deps-regen.js +327 -0
- package/tsconfig/node/dist/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
const require_dependency_table = require('../utils/dependency-table.cjs');
|
|
2
|
+
const require_index = require('../../../../../../../node_modules/.pnpm/workspaces-effect@1.2.0_@effect_platform@0.96.2_effect@3.21.4__effect@3.21.4/node_modules/workspaces-effect/index.cjs');
|
|
3
|
+
const require_config_inspector = require('./config-inspector.cjs');
|
|
4
|
+
const require_dep_diff = require('../utils/dep-diff.cjs');
|
|
5
|
+
const require_publishability = require('../utils/publishability.cjs');
|
|
6
|
+
const require_worktree_snapshot = require('../utils/worktree-snapshot.cjs');
|
|
7
|
+
const require_workspace_snapshot = require('./workspace-snapshot.cjs');
|
|
8
|
+
let effect = require("effect");
|
|
9
|
+
let node_path = require("node:path");
|
|
10
|
+
let node_fs = require("node:fs");
|
|
11
|
+
|
|
12
|
+
//#region ../silk-effects/dist/dev/pkg/changesets/services/deps-regen.js
|
|
13
|
+
/**
|
|
14
|
+
* `Changesets.DepsRegen` service — lift the `deps regen` / `deps detect`
|
|
15
|
+
* orchestration out of the CLI into a `Context.Tag` service with a
|
|
16
|
+
* `plan()` / `execute()` split.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* `plan()` computes the cumulative dependency diff (merge-base → working
|
|
20
|
+
* tree by default, or between two explicit refs), resolves `catalog:` /
|
|
21
|
+
* `workspace:` specifiers to concrete versions, drops `devDependency`
|
|
22
|
+
* rows (unless `includeDevDeps`), and returns a complete {@link RegenPlan}
|
|
23
|
+
* (target filenames + stale-changeset deletes) WITHOUT touching the
|
|
24
|
+
* filesystem. `execute()` applies a plan — writing the fresh changesets
|
|
25
|
+
* first, then deleting the stale pure-dependency ones (so an interrupted
|
|
26
|
+
* run loses nothing and is safely re-runnable).
|
|
27
|
+
*
|
|
28
|
+
* This is the single source of truth for regen/detect: the CLI commands
|
|
29
|
+
* and MCP tools are thin adapters over this service.
|
|
30
|
+
*
|
|
31
|
+
* @see {@link DepsRegen} for the service tag
|
|
32
|
+
* @see {@link DepsRegenLive} for the production layer
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
/** The em-dash sentinel (U+2014) used for added ("from") / removed ("to") cells. */
|
|
36
|
+
const EM_DASH = "—";
|
|
37
|
+
/** Whether a From/To cell holds a pnpm protocol specifier that could resolve to a version. */
|
|
38
|
+
const isProtocol = (v) => /^(?:catalog|workspace|npm|jsr|file|link|portal):/.test(v);
|
|
39
|
+
/**
|
|
40
|
+
* Resolve protocol From/To cells to concrete versions (raw-string fallback
|
|
41
|
+
* when unresolved or on resolver error), leave em-dash sentinels untouched,
|
|
42
|
+
* then optionally drop `devDependency` rows, and re-sort.
|
|
43
|
+
*
|
|
44
|
+
* @param diff - One workspace package's dependency-table rows.
|
|
45
|
+
* @param keepDevDeps - When `true`, retain `devDependency` rows; otherwise
|
|
46
|
+
* drop them unconditionally (the regen default).
|
|
47
|
+
* @returns An Effect yielding the transformed {@link WorkspaceDependencyDiff}.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
const resolveDiffRows = (diff, keepDevDeps = false) => effect.Effect.gen(function* () {
|
|
52
|
+
const resolver = yield* require_index.CatalogResolver;
|
|
53
|
+
const resolveCell = (dep, value) => isProtocol(value) ? resolver.resolveSpecifier(dep, value).pipe(effect.Effect.map((opt) => effect.Option.getOrElse(opt, () => value)), effect.Effect.catchAll((error) => effect.Effect.logWarning(`DepsRegen: catalog resolution failed for "${dep}" (${value}); keeping raw specifier: ${String(error)}`).pipe(effect.Effect.as(value)))) : effect.Effect.succeed(value);
|
|
54
|
+
const rows = [];
|
|
55
|
+
for (const row of diff.rows) {
|
|
56
|
+
if (!keepDevDeps && row.type === "devDependency") continue;
|
|
57
|
+
const from = row.from === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.from);
|
|
58
|
+
const to = row.to === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.to);
|
|
59
|
+
rows.push({
|
|
60
|
+
...row,
|
|
61
|
+
from,
|
|
62
|
+
to
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...diff,
|
|
67
|
+
rows: require_dependency_table.sortDependencyRows(rows)
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
const ADJECTIVES = [
|
|
71
|
+
"brave",
|
|
72
|
+
"clever",
|
|
73
|
+
"swift",
|
|
74
|
+
"silver",
|
|
75
|
+
"lucky",
|
|
76
|
+
"happy",
|
|
77
|
+
"calm",
|
|
78
|
+
"bright",
|
|
79
|
+
"quiet",
|
|
80
|
+
"wild"
|
|
81
|
+
];
|
|
82
|
+
const NOUNS = [
|
|
83
|
+
"dogs",
|
|
84
|
+
"cats",
|
|
85
|
+
"wolves",
|
|
86
|
+
"foxes",
|
|
87
|
+
"cups",
|
|
88
|
+
"ships",
|
|
89
|
+
"trees",
|
|
90
|
+
"owls",
|
|
91
|
+
"cranes",
|
|
92
|
+
"hills"
|
|
93
|
+
];
|
|
94
|
+
const VERBS = [
|
|
95
|
+
"laugh",
|
|
96
|
+
"dream",
|
|
97
|
+
"fly",
|
|
98
|
+
"sing",
|
|
99
|
+
"dance",
|
|
100
|
+
"wander",
|
|
101
|
+
"soar",
|
|
102
|
+
"rest",
|
|
103
|
+
"leap",
|
|
104
|
+
"ponder"
|
|
105
|
+
];
|
|
106
|
+
function pickRandomTriplet() {
|
|
107
|
+
return `${ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]}-${NOUNS[Math.floor(Math.random() * NOUNS.length)]}-${VERBS[Math.floor(Math.random() * VERBS.length)]}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Pick a `<adjective>-<noun>-<verb>` filename slug that does not collide
|
|
111
|
+
* with an existing `.changeset/*.md` OR with a slug already claimed
|
|
112
|
+
* earlier in the same {@link RegenPlan.toWrite} computation. `plan()`
|
|
113
|
+
* never writes to disk, so `existsSync` alone cannot see slugs chosen
|
|
114
|
+
* moments earlier in the same call — the `chosen` set closes that gap.
|
|
115
|
+
* The triplet space is 1,000 combinations, so a busy repo can plausibly
|
|
116
|
+
* exhaust it across runs; fall back to a timestamp suffix after 20
|
|
117
|
+
* unlucky picks.
|
|
118
|
+
*
|
|
119
|
+
* @param changesetDir - Directory checked via `existsSync` for on-disk collisions.
|
|
120
|
+
* @param chosen - Basenames (without extension) already picked within this plan;
|
|
121
|
+
* the picked candidate is added to this set before returning.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
function randomFilename(changesetDir, chosen) {
|
|
125
|
+
for (let i = 0; i < 20; i++) {
|
|
126
|
+
const candidate = pickRandomTriplet();
|
|
127
|
+
if (!chosen.has(candidate) && !(0, node_fs.existsSync)((0, node_path.join)(changesetDir, `${candidate}.md`))) {
|
|
128
|
+
chosen.add(candidate);
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let attempt = 0;
|
|
133
|
+
let fallback = `${pickRandomTriplet()}-${Date.now()}`;
|
|
134
|
+
while (chosen.has(fallback) || (0, node_fs.existsSync)((0, node_path.join)(changesetDir, `${fallback}.md`))) fallback = `${pickRandomTriplet()}-${Date.now()}-${++attempt}`;
|
|
135
|
+
chosen.add(fallback);
|
|
136
|
+
return fallback;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Strict detection of "pure dependency changesets" per the documented
|
|
140
|
+
* rules: single-package frontmatter, single `## Dependencies` heading,
|
|
141
|
+
* no other body content beyond that section.
|
|
142
|
+
*
|
|
143
|
+
* @param content - Raw `.changeset/*.md` file contents.
|
|
144
|
+
* @returns `{ isPure, package }` — `isPure` is `true` only for a
|
|
145
|
+
* single-package, Dependencies-only changeset; `package` is the sole
|
|
146
|
+
* frontmatter package name (or `null` when not pure).
|
|
147
|
+
*
|
|
148
|
+
* @public
|
|
149
|
+
*/
|
|
150
|
+
function isPureDependencyChangeset(content) {
|
|
151
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
152
|
+
if (!fmMatch) return {
|
|
153
|
+
isPure: false,
|
|
154
|
+
package: null
|
|
155
|
+
};
|
|
156
|
+
const frontmatter = fmMatch[1];
|
|
157
|
+
const body = (fmMatch[2] ?? "").trim();
|
|
158
|
+
const fmLines = frontmatter.split(/\r?\n/).filter((l) => l.trim().length > 0 && !/^\s*#/.test(l));
|
|
159
|
+
if (fmLines.length !== 1) return {
|
|
160
|
+
isPure: false,
|
|
161
|
+
package: null
|
|
162
|
+
};
|
|
163
|
+
const pkgMatch = fmLines[0].match(/^\s*["']?([^"':\s]+)["']?\s*:\s*([a-z]+)\s*$/);
|
|
164
|
+
if (!pkgMatch) return {
|
|
165
|
+
isPure: false,
|
|
166
|
+
package: null
|
|
167
|
+
};
|
|
168
|
+
const pkg = pkgMatch[1];
|
|
169
|
+
const bodyTrimmed = body.replace(/^\s+/, "");
|
|
170
|
+
if (!/^## Dependencies\b/.test(bodyTrimmed)) return {
|
|
171
|
+
isPure: false,
|
|
172
|
+
package: null
|
|
173
|
+
};
|
|
174
|
+
if ((bodyTrimmed.match(/^## /gm) ?? []).length !== 1) return {
|
|
175
|
+
isPure: false,
|
|
176
|
+
package: null
|
|
177
|
+
};
|
|
178
|
+
if (/^# /m.test(bodyTrimmed)) return {
|
|
179
|
+
isPure: false,
|
|
180
|
+
package: null
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
isPure: true,
|
|
184
|
+
package: pkg
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function listChangesetFiles(changesetDir) {
|
|
188
|
+
if (!(0, node_fs.existsSync)(changesetDir)) return [];
|
|
189
|
+
return (0, node_fs.readdirSync)(changesetDir).filter((f) => f.endsWith(".md") && f !== "README.md").map((f) => (0, node_path.join)(changesetDir, f));
|
|
190
|
+
}
|
|
191
|
+
function findPureDependencyChangesets(changesetDir) {
|
|
192
|
+
const result = [];
|
|
193
|
+
for (const file of listChangesetFiles(changesetDir)) {
|
|
194
|
+
let content;
|
|
195
|
+
try {
|
|
196
|
+
content = (0, node_fs.readFileSync)(file, "utf8");
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const detection = isPureDependencyChangeset(content);
|
|
201
|
+
if (detection.isPure && detection.package) result.push({
|
|
202
|
+
file,
|
|
203
|
+
package: detection.package
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
function findMixedDependencyChangesets(changesetDir) {
|
|
209
|
+
const result = [];
|
|
210
|
+
for (const file of listChangesetFiles(changesetDir)) {
|
|
211
|
+
let content;
|
|
212
|
+
try {
|
|
213
|
+
content = (0, node_fs.readFileSync)(file, "utf8");
|
|
214
|
+
} catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (/^## Dependencies\b/m.test(content) && !isPureDependencyChangeset(content).isPure) result.push(file);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Render a single-package, patch-bump changeset for a diff whose rows have
|
|
223
|
+
* already been resolved/filtered by {@link resolveDiffRows}.
|
|
224
|
+
*/
|
|
225
|
+
function renderChangesetContent(diff) {
|
|
226
|
+
return `${`---\n"${diff.package}": patch\n---`}\n\n## Dependencies\n\n${require_dependency_table.serializeDependencyTableToMarkdown([...diff.rows])}\n`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* @internal
|
|
230
|
+
*/
|
|
231
|
+
const DepsRegenBase = effect.Context.Tag("Changesets/DepsRegen")();
|
|
232
|
+
/**
|
|
233
|
+
* Effect service tag for {@link DepsRegenShape}.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* import { Effect } from "effect";
|
|
238
|
+
* import { Changesets } from "@savvy-web/silk-effects";
|
|
239
|
+
*
|
|
240
|
+
* const program = Effect.gen(function* () {
|
|
241
|
+
* const svc = yield* Changesets.DepsRegen;
|
|
242
|
+
* const plan = yield* svc.plan({ cwd: process.cwd() });
|
|
243
|
+
* return yield* svc.execute(plan);
|
|
244
|
+
* });
|
|
245
|
+
* ```
|
|
246
|
+
*
|
|
247
|
+
* @public
|
|
248
|
+
*/
|
|
249
|
+
var DepsRegen = class extends DepsRegenBase {};
|
|
250
|
+
/**
|
|
251
|
+
* Build a {@link DepsRegenShape} that closes over already-resolved service
|
|
252
|
+
* implementations, keeping the public `plan`/`execute` signatures
|
|
253
|
+
* requirement-free (`R = never`).
|
|
254
|
+
*/
|
|
255
|
+
function makeShape(reader, inspector, discovery, resolver, detector) {
|
|
256
|
+
const provideResolver = effect.Layer.succeed(require_index.CatalogResolver, resolver);
|
|
257
|
+
const provideDetector = effect.Layer.succeed(require_index.PublishabilityDetector, detector);
|
|
258
|
+
const plan = (options) => effect.Effect.gen(function* () {
|
|
259
|
+
const resolvedCwd = (0, node_path.resolve)(options.cwd);
|
|
260
|
+
const changesetDir = (0, node_path.join)(resolvedCwd, ".changeset");
|
|
261
|
+
let fromRef = options.from;
|
|
262
|
+
if (!fromRef) {
|
|
263
|
+
let baseBranch = options.base;
|
|
264
|
+
if (!baseBranch) baseBranch = (yield* inspector.inspect(resolvedCwd).pipe(effect.Effect.catchTag("ConfigurationError", () => effect.Effect.succeed({ baseBranch: "main" })))).baseBranch;
|
|
265
|
+
fromRef = yield* require_worktree_snapshot.gitMergeBase(resolvedCwd, baseBranch);
|
|
266
|
+
}
|
|
267
|
+
const rawDiffs = require_dep_diff.computeWorkspaceDependencyDiffs(yield* reader.snapshotAt(resolvedCwd, fromRef), options.to ? yield* reader.snapshotAt(resolvedCwd, options.to) : require_worktree_snapshot.snapshotFromWorktree(resolvedCwd));
|
|
268
|
+
const targetPkg = options.package;
|
|
269
|
+
const publishable = yield* require_publishability.listPublishablePackageNames(yield* discovery.listPackages(resolvedCwd)).pipe(effect.Effect.provide(provideDetector));
|
|
270
|
+
const keepDevDeps = options.includeDevDeps === true;
|
|
271
|
+
const scoped = targetPkg ? rawDiffs.filter((d) => d.package === targetPkg) : rawDiffs.filter((d) => publishable.has(d.package));
|
|
272
|
+
const resolved = [];
|
|
273
|
+
for (const diff of scoped) {
|
|
274
|
+
const next = yield* resolveDiffRows(diff, keepDevDeps).pipe(effect.Effect.provide(provideResolver));
|
|
275
|
+
if (next.rows.length > 0) resolved.push(next);
|
|
276
|
+
}
|
|
277
|
+
const existingPure = findPureDependencyChangesets(changesetDir);
|
|
278
|
+
const skippedMixed = findMixedDependencyChangesets(changesetDir);
|
|
279
|
+
const toDelete = targetPkg ? existingPure.filter((p) => p.package === targetPkg) : existingPure.filter((p) => publishable.has(p.package));
|
|
280
|
+
const chosenFilenames = /* @__PURE__ */ new Set();
|
|
281
|
+
return {
|
|
282
|
+
toDelete,
|
|
283
|
+
toWrite: resolved.map((diff) => ({
|
|
284
|
+
file: (0, node_path.join)(changesetDir, `${randomFilename(changesetDir, chosenFilenames)}.md`),
|
|
285
|
+
package: diff.package,
|
|
286
|
+
diff
|
|
287
|
+
})),
|
|
288
|
+
skippedMixed
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
const execute = (plan) => effect.Effect.sync(() => {
|
|
292
|
+
const deleted = [];
|
|
293
|
+
const written = [];
|
|
294
|
+
for (const entry of plan.toWrite) {
|
|
295
|
+
(0, node_fs.writeFileSync)(entry.file, renderChangesetContent(entry.diff));
|
|
296
|
+
written.push(entry.file);
|
|
297
|
+
}
|
|
298
|
+
for (const entry of plan.toDelete) try {
|
|
299
|
+
(0, node_fs.unlinkSync)(entry.file);
|
|
300
|
+
deleted.push(entry.file);
|
|
301
|
+
} catch {}
|
|
302
|
+
return {
|
|
303
|
+
deleted,
|
|
304
|
+
written,
|
|
305
|
+
skippedMixed: plan.skippedMixed
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
plan,
|
|
310
|
+
execute
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Live layer for {@link DepsRegen}.
|
|
315
|
+
*
|
|
316
|
+
* Requires {@link WorkspaceSnapshotReader}, {@link ConfigInspector},
|
|
317
|
+
* `WorkspaceDiscovery`, `CatalogResolver`, and `PublishabilityDetector`
|
|
318
|
+
* (the last three from `workspaces-effect`).
|
|
319
|
+
*
|
|
320
|
+
* @public
|
|
321
|
+
*/
|
|
322
|
+
const DepsRegenLive = effect.Layer.effect(DepsRegen, effect.Effect.gen(function* () {
|
|
323
|
+
return makeShape(yield* require_workspace_snapshot.WorkspaceSnapshotReader, yield* require_config_inspector.ConfigInspector, yield* require_index.WorkspaceDiscovery, yield* require_index.CatalogResolver, yield* require_index.PublishabilityDetector);
|
|
324
|
+
}));
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
exports.DepsRegen = DepsRegen;
|
|
328
|
+
exports.DepsRegenBase = DepsRegenBase;
|
|
329
|
+
exports.DepsRegenLive = DepsRegenLive;
|
|
330
|
+
exports.isPureDependencyChangeset = isPureDependencyChangeset;
|
|
331
|
+
exports.resolveDiffRows = resolveDiffRows;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { serializeDependencyTableToMarkdown, sortDependencyRows } from "../utils/dependency-table.js";
|
|
2
|
+
import { CatalogResolver, PublishabilityDetector, WorkspaceDiscovery } from "../../../../../../../node_modules/.pnpm/workspaces-effect@1.2.0_@effect_platform@0.96.2_effect@3.21.4__effect@3.21.4/node_modules/workspaces-effect/index.js";
|
|
3
|
+
import { ConfigInspector } from "./config-inspector.js";
|
|
4
|
+
import { computeWorkspaceDependencyDiffs } from "../utils/dep-diff.js";
|
|
5
|
+
import { listPublishablePackageNames } from "../utils/publishability.js";
|
|
6
|
+
import { gitMergeBase, snapshotFromWorktree } from "../utils/worktree-snapshot.js";
|
|
7
|
+
import { WorkspaceSnapshotReader } from "./workspace-snapshot.js";
|
|
8
|
+
import { Context, Effect, Layer, Option } from "effect";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
//#region ../silk-effects/dist/dev/pkg/changesets/services/deps-regen.js
|
|
13
|
+
/**
|
|
14
|
+
* `Changesets.DepsRegen` service — lift the `deps regen` / `deps detect`
|
|
15
|
+
* orchestration out of the CLI into a `Context.Tag` service with a
|
|
16
|
+
* `plan()` / `execute()` split.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* `plan()` computes the cumulative dependency diff (merge-base → working
|
|
20
|
+
* tree by default, or between two explicit refs), resolves `catalog:` /
|
|
21
|
+
* `workspace:` specifiers to concrete versions, drops `devDependency`
|
|
22
|
+
* rows (unless `includeDevDeps`), and returns a complete {@link RegenPlan}
|
|
23
|
+
* (target filenames + stale-changeset deletes) WITHOUT touching the
|
|
24
|
+
* filesystem. `execute()` applies a plan — writing the fresh changesets
|
|
25
|
+
* first, then deleting the stale pure-dependency ones (so an interrupted
|
|
26
|
+
* run loses nothing and is safely re-runnable).
|
|
27
|
+
*
|
|
28
|
+
* This is the single source of truth for regen/detect: the CLI commands
|
|
29
|
+
* and MCP tools are thin adapters over this service.
|
|
30
|
+
*
|
|
31
|
+
* @see {@link DepsRegen} for the service tag
|
|
32
|
+
* @see {@link DepsRegenLive} for the production layer
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
/** The em-dash sentinel (U+2014) used for added ("from") / removed ("to") cells. */
|
|
36
|
+
const EM_DASH = "—";
|
|
37
|
+
/** Whether a From/To cell holds a pnpm protocol specifier that could resolve to a version. */
|
|
38
|
+
const isProtocol = (v) => /^(?:catalog|workspace|npm|jsr|file|link|portal):/.test(v);
|
|
39
|
+
/**
|
|
40
|
+
* Resolve protocol From/To cells to concrete versions (raw-string fallback
|
|
41
|
+
* when unresolved or on resolver error), leave em-dash sentinels untouched,
|
|
42
|
+
* then optionally drop `devDependency` rows, and re-sort.
|
|
43
|
+
*
|
|
44
|
+
* @param diff - One workspace package's dependency-table rows.
|
|
45
|
+
* @param keepDevDeps - When `true`, retain `devDependency` rows; otherwise
|
|
46
|
+
* drop them unconditionally (the regen default).
|
|
47
|
+
* @returns An Effect yielding the transformed {@link WorkspaceDependencyDiff}.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
const resolveDiffRows = (diff, keepDevDeps = false) => Effect.gen(function* () {
|
|
52
|
+
const resolver = yield* CatalogResolver;
|
|
53
|
+
const resolveCell = (dep, value) => isProtocol(value) ? resolver.resolveSpecifier(dep, value).pipe(Effect.map((opt) => Option.getOrElse(opt, () => value)), Effect.catchAll((error) => Effect.logWarning(`DepsRegen: catalog resolution failed for "${dep}" (${value}); keeping raw specifier: ${String(error)}`).pipe(Effect.as(value)))) : Effect.succeed(value);
|
|
54
|
+
const rows = [];
|
|
55
|
+
for (const row of diff.rows) {
|
|
56
|
+
if (!keepDevDeps && row.type === "devDependency") continue;
|
|
57
|
+
const from = row.from === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.from);
|
|
58
|
+
const to = row.to === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.to);
|
|
59
|
+
rows.push({
|
|
60
|
+
...row,
|
|
61
|
+
from,
|
|
62
|
+
to
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...diff,
|
|
67
|
+
rows: sortDependencyRows(rows)
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
const ADJECTIVES = [
|
|
71
|
+
"brave",
|
|
72
|
+
"clever",
|
|
73
|
+
"swift",
|
|
74
|
+
"silver",
|
|
75
|
+
"lucky",
|
|
76
|
+
"happy",
|
|
77
|
+
"calm",
|
|
78
|
+
"bright",
|
|
79
|
+
"quiet",
|
|
80
|
+
"wild"
|
|
81
|
+
];
|
|
82
|
+
const NOUNS = [
|
|
83
|
+
"dogs",
|
|
84
|
+
"cats",
|
|
85
|
+
"wolves",
|
|
86
|
+
"foxes",
|
|
87
|
+
"cups",
|
|
88
|
+
"ships",
|
|
89
|
+
"trees",
|
|
90
|
+
"owls",
|
|
91
|
+
"cranes",
|
|
92
|
+
"hills"
|
|
93
|
+
];
|
|
94
|
+
const VERBS = [
|
|
95
|
+
"laugh",
|
|
96
|
+
"dream",
|
|
97
|
+
"fly",
|
|
98
|
+
"sing",
|
|
99
|
+
"dance",
|
|
100
|
+
"wander",
|
|
101
|
+
"soar",
|
|
102
|
+
"rest",
|
|
103
|
+
"leap",
|
|
104
|
+
"ponder"
|
|
105
|
+
];
|
|
106
|
+
function pickRandomTriplet() {
|
|
107
|
+
return `${ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]}-${NOUNS[Math.floor(Math.random() * NOUNS.length)]}-${VERBS[Math.floor(Math.random() * VERBS.length)]}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Pick a `<adjective>-<noun>-<verb>` filename slug that does not collide
|
|
111
|
+
* with an existing `.changeset/*.md` OR with a slug already claimed
|
|
112
|
+
* earlier in the same {@link RegenPlan.toWrite} computation. `plan()`
|
|
113
|
+
* never writes to disk, so `existsSync` alone cannot see slugs chosen
|
|
114
|
+
* moments earlier in the same call — the `chosen` set closes that gap.
|
|
115
|
+
* The triplet space is 1,000 combinations, so a busy repo can plausibly
|
|
116
|
+
* exhaust it across runs; fall back to a timestamp suffix after 20
|
|
117
|
+
* unlucky picks.
|
|
118
|
+
*
|
|
119
|
+
* @param changesetDir - Directory checked via `existsSync` for on-disk collisions.
|
|
120
|
+
* @param chosen - Basenames (without extension) already picked within this plan;
|
|
121
|
+
* the picked candidate is added to this set before returning.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
function randomFilename(changesetDir, chosen) {
|
|
125
|
+
for (let i = 0; i < 20; i++) {
|
|
126
|
+
const candidate = pickRandomTriplet();
|
|
127
|
+
if (!chosen.has(candidate) && !existsSync(join(changesetDir, `${candidate}.md`))) {
|
|
128
|
+
chosen.add(candidate);
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let attempt = 0;
|
|
133
|
+
let fallback = `${pickRandomTriplet()}-${Date.now()}`;
|
|
134
|
+
while (chosen.has(fallback) || existsSync(join(changesetDir, `${fallback}.md`))) fallback = `${pickRandomTriplet()}-${Date.now()}-${++attempt}`;
|
|
135
|
+
chosen.add(fallback);
|
|
136
|
+
return fallback;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Strict detection of "pure dependency changesets" per the documented
|
|
140
|
+
* rules: single-package frontmatter, single `## Dependencies` heading,
|
|
141
|
+
* no other body content beyond that section.
|
|
142
|
+
*
|
|
143
|
+
* @param content - Raw `.changeset/*.md` file contents.
|
|
144
|
+
* @returns `{ isPure, package }` — `isPure` is `true` only for a
|
|
145
|
+
* single-package, Dependencies-only changeset; `package` is the sole
|
|
146
|
+
* frontmatter package name (or `null` when not pure).
|
|
147
|
+
*
|
|
148
|
+
* @public
|
|
149
|
+
*/
|
|
150
|
+
function isPureDependencyChangeset(content) {
|
|
151
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
152
|
+
if (!fmMatch) return {
|
|
153
|
+
isPure: false,
|
|
154
|
+
package: null
|
|
155
|
+
};
|
|
156
|
+
const frontmatter = fmMatch[1];
|
|
157
|
+
const body = (fmMatch[2] ?? "").trim();
|
|
158
|
+
const fmLines = frontmatter.split(/\r?\n/).filter((l) => l.trim().length > 0 && !/^\s*#/.test(l));
|
|
159
|
+
if (fmLines.length !== 1) return {
|
|
160
|
+
isPure: false,
|
|
161
|
+
package: null
|
|
162
|
+
};
|
|
163
|
+
const pkgMatch = fmLines[0].match(/^\s*["']?([^"':\s]+)["']?\s*:\s*([a-z]+)\s*$/);
|
|
164
|
+
if (!pkgMatch) return {
|
|
165
|
+
isPure: false,
|
|
166
|
+
package: null
|
|
167
|
+
};
|
|
168
|
+
const pkg = pkgMatch[1];
|
|
169
|
+
const bodyTrimmed = body.replace(/^\s+/, "");
|
|
170
|
+
if (!/^## Dependencies\b/.test(bodyTrimmed)) return {
|
|
171
|
+
isPure: false,
|
|
172
|
+
package: null
|
|
173
|
+
};
|
|
174
|
+
if ((bodyTrimmed.match(/^## /gm) ?? []).length !== 1) return {
|
|
175
|
+
isPure: false,
|
|
176
|
+
package: null
|
|
177
|
+
};
|
|
178
|
+
if (/^# /m.test(bodyTrimmed)) return {
|
|
179
|
+
isPure: false,
|
|
180
|
+
package: null
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
isPure: true,
|
|
184
|
+
package: pkg
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function listChangesetFiles(changesetDir) {
|
|
188
|
+
if (!existsSync(changesetDir)) return [];
|
|
189
|
+
return readdirSync(changesetDir).filter((f) => f.endsWith(".md") && f !== "README.md").map((f) => join(changesetDir, f));
|
|
190
|
+
}
|
|
191
|
+
function findPureDependencyChangesets(changesetDir) {
|
|
192
|
+
const result = [];
|
|
193
|
+
for (const file of listChangesetFiles(changesetDir)) {
|
|
194
|
+
let content;
|
|
195
|
+
try {
|
|
196
|
+
content = readFileSync(file, "utf8");
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const detection = isPureDependencyChangeset(content);
|
|
201
|
+
if (detection.isPure && detection.package) result.push({
|
|
202
|
+
file,
|
|
203
|
+
package: detection.package
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
function findMixedDependencyChangesets(changesetDir) {
|
|
209
|
+
const result = [];
|
|
210
|
+
for (const file of listChangesetFiles(changesetDir)) {
|
|
211
|
+
let content;
|
|
212
|
+
try {
|
|
213
|
+
content = readFileSync(file, "utf8");
|
|
214
|
+
} catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (/^## Dependencies\b/m.test(content) && !isPureDependencyChangeset(content).isPure) result.push(file);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Render a single-package, patch-bump changeset for a diff whose rows have
|
|
223
|
+
* already been resolved/filtered by {@link resolveDiffRows}.
|
|
224
|
+
*/
|
|
225
|
+
function renderChangesetContent(diff) {
|
|
226
|
+
return `${`---\n"${diff.package}": patch\n---`}\n\n## Dependencies\n\n${serializeDependencyTableToMarkdown([...diff.rows])}\n`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* @internal
|
|
230
|
+
*/
|
|
231
|
+
const DepsRegenBase = Context.Tag("Changesets/DepsRegen")();
|
|
232
|
+
/**
|
|
233
|
+
* Effect service tag for {@link DepsRegenShape}.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* import { Effect } from "effect";
|
|
238
|
+
* import { Changesets } from "@savvy-web/silk-effects";
|
|
239
|
+
*
|
|
240
|
+
* const program = Effect.gen(function* () {
|
|
241
|
+
* const svc = yield* Changesets.DepsRegen;
|
|
242
|
+
* const plan = yield* svc.plan({ cwd: process.cwd() });
|
|
243
|
+
* return yield* svc.execute(plan);
|
|
244
|
+
* });
|
|
245
|
+
* ```
|
|
246
|
+
*
|
|
247
|
+
* @public
|
|
248
|
+
*/
|
|
249
|
+
var DepsRegen = class extends DepsRegenBase {};
|
|
250
|
+
/**
|
|
251
|
+
* Build a {@link DepsRegenShape} that closes over already-resolved service
|
|
252
|
+
* implementations, keeping the public `plan`/`execute` signatures
|
|
253
|
+
* requirement-free (`R = never`).
|
|
254
|
+
*/
|
|
255
|
+
function makeShape(reader, inspector, discovery, resolver, detector) {
|
|
256
|
+
const provideResolver = Layer.succeed(CatalogResolver, resolver);
|
|
257
|
+
const provideDetector = Layer.succeed(PublishabilityDetector, detector);
|
|
258
|
+
const plan = (options) => Effect.gen(function* () {
|
|
259
|
+
const resolvedCwd = resolve(options.cwd);
|
|
260
|
+
const changesetDir = join(resolvedCwd, ".changeset");
|
|
261
|
+
let fromRef = options.from;
|
|
262
|
+
if (!fromRef) {
|
|
263
|
+
let baseBranch = options.base;
|
|
264
|
+
if (!baseBranch) baseBranch = (yield* inspector.inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", () => Effect.succeed({ baseBranch: "main" })))).baseBranch;
|
|
265
|
+
fromRef = yield* gitMergeBase(resolvedCwd, baseBranch);
|
|
266
|
+
}
|
|
267
|
+
const rawDiffs = computeWorkspaceDependencyDiffs(yield* reader.snapshotAt(resolvedCwd, fromRef), options.to ? yield* reader.snapshotAt(resolvedCwd, options.to) : snapshotFromWorktree(resolvedCwd));
|
|
268
|
+
const targetPkg = options.package;
|
|
269
|
+
const publishable = yield* listPublishablePackageNames(yield* discovery.listPackages(resolvedCwd)).pipe(Effect.provide(provideDetector));
|
|
270
|
+
const keepDevDeps = options.includeDevDeps === true;
|
|
271
|
+
const scoped = targetPkg ? rawDiffs.filter((d) => d.package === targetPkg) : rawDiffs.filter((d) => publishable.has(d.package));
|
|
272
|
+
const resolved = [];
|
|
273
|
+
for (const diff of scoped) {
|
|
274
|
+
const next = yield* resolveDiffRows(diff, keepDevDeps).pipe(Effect.provide(provideResolver));
|
|
275
|
+
if (next.rows.length > 0) resolved.push(next);
|
|
276
|
+
}
|
|
277
|
+
const existingPure = findPureDependencyChangesets(changesetDir);
|
|
278
|
+
const skippedMixed = findMixedDependencyChangesets(changesetDir);
|
|
279
|
+
const toDelete = targetPkg ? existingPure.filter((p) => p.package === targetPkg) : existingPure.filter((p) => publishable.has(p.package));
|
|
280
|
+
const chosenFilenames = /* @__PURE__ */ new Set();
|
|
281
|
+
return {
|
|
282
|
+
toDelete,
|
|
283
|
+
toWrite: resolved.map((diff) => ({
|
|
284
|
+
file: join(changesetDir, `${randomFilename(changesetDir, chosenFilenames)}.md`),
|
|
285
|
+
package: diff.package,
|
|
286
|
+
diff
|
|
287
|
+
})),
|
|
288
|
+
skippedMixed
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
const execute = (plan) => Effect.sync(() => {
|
|
292
|
+
const deleted = [];
|
|
293
|
+
const written = [];
|
|
294
|
+
for (const entry of plan.toWrite) {
|
|
295
|
+
writeFileSync(entry.file, renderChangesetContent(entry.diff));
|
|
296
|
+
written.push(entry.file);
|
|
297
|
+
}
|
|
298
|
+
for (const entry of plan.toDelete) try {
|
|
299
|
+
unlinkSync(entry.file);
|
|
300
|
+
deleted.push(entry.file);
|
|
301
|
+
} catch {}
|
|
302
|
+
return {
|
|
303
|
+
deleted,
|
|
304
|
+
written,
|
|
305
|
+
skippedMixed: plan.skippedMixed
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
plan,
|
|
310
|
+
execute
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Live layer for {@link DepsRegen}.
|
|
315
|
+
*
|
|
316
|
+
* Requires {@link WorkspaceSnapshotReader}, {@link ConfigInspector},
|
|
317
|
+
* `WorkspaceDiscovery`, `CatalogResolver`, and `PublishabilityDetector`
|
|
318
|
+
* (the last three from `workspaces-effect`).
|
|
319
|
+
*
|
|
320
|
+
* @public
|
|
321
|
+
*/
|
|
322
|
+
const DepsRegenLive = Layer.effect(DepsRegen, Effect.gen(function* () {
|
|
323
|
+
return makeShape(yield* WorkspaceSnapshotReader, yield* ConfigInspector, yield* WorkspaceDiscovery, yield* CatalogResolver, yield* PublishabilityDetector);
|
|
324
|
+
}));
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
export { DepsRegen, DepsRegenBase, DepsRegenLive, isPureDependencyChangeset, resolveDiffRows };
|