@savvy-web/silk-effects 5.0.1 → 5.1.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/README.md +69 -0
- package/changesets/services/release-planner.js +44 -26
- package/index.d.ts +16 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,17 @@ const tags = strategy.tagsFor([{ name: "@savvy-web/silk-effects", version: "1.0.
|
|
|
107
107
|
|
|
108
108
|
`WorkspaceAnalysis.versioning` and `WorkspaceAnalysis.tagStrategy` carry those kit types directly.
|
|
109
109
|
|
|
110
|
+
#### ChangesetLinter
|
|
111
|
+
|
|
112
|
+
Validate a changeset file against the Silk section rules. `ChangesetLinter.validateContent(content, filePath?)` and `ChangesetLinter.validateFile(filePath)` are static and synchronous, returning `LintMessage[]` — no Effect, no layers. Rules cover the valid section headings, structural constraints, and the dependency-table format, so a `## Dependencies` section written as prose or a bullet list is reported rather than accepted.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { Changesets } from "@savvy-web/silk-effects";
|
|
116
|
+
|
|
117
|
+
const messages = Changesets.ChangesetLinter.validateFile(".changeset/quiet-moons-render.md");
|
|
118
|
+
// => [] when the file is valid, otherwise one LintMessage per violation
|
|
119
|
+
```
|
|
120
|
+
|
|
110
121
|
---
|
|
111
122
|
|
|
112
123
|
### FileSystem layer required
|
|
@@ -258,10 +269,68 @@ const result = await Effect.runPromise(
|
|
|
258
269
|
// => { updated: true, skipped: false, current: "2.0.0" }
|
|
259
270
|
```
|
|
260
271
|
|
|
272
|
+
#### ConfigInspector
|
|
273
|
+
|
|
274
|
+
Resolve `.changeset/config.json` into a fully attributed view of the workspace: the configured changelog, base branch, access and ignore list, plus one scope per package carrying its `workspaceDir`, version, `additionalScopes` and resolved `versionFiles`. `inspect(cwd)` returns that view and `classify` maps arbitrary file paths to the package that owns them, which is how a branch diff becomes a per-package attribution. `refresh()` clears the per-root cache, which a long-lived host needs in order to see config edits made between calls.
|
|
275
|
+
|
|
276
|
+
`Changesets.ConfigInspectorLive` requires `ChangesetConfigReader`, `WorkspaceDiscovery` from [`@effected/workspaces`](https://www.npmjs.com/package/@effected/workspaces) and `FileSystem`.
|
|
277
|
+
|
|
278
|
+
#### ReleasePlanner
|
|
279
|
+
|
|
280
|
+
Drive the genuine changesets engine rather than shelling out to the `changeset` binary. Three members:
|
|
281
|
+
|
|
282
|
+
- `plan(root)` computes the in-memory release plan. It renders nothing, so it resolves no changelog module.
|
|
283
|
+
- `preview(root, options?)` renders a non-destructive preview, running the real engine against a scope-managed temp directory and reading the generated CHANGELOG blocks back. The repository is never mutated.
|
|
284
|
+
- `apply(root, options?)` performs the release — version bumps, CHANGELOG writes and configured version-file updates. Pass `dryRun` to compute without writing.
|
|
285
|
+
|
|
286
|
+
Both `preview` and `apply` accept `changelogModules`, mapping the changelog id configured in `.changeset/config.json` to an absolute module path. Reach for it when running somewhere the configured id cannot be resolved — a bundled GitHub Action with no `node_modules`, for instance. When set, the configured id must be a key of the map, an unmapped id fails with a `ReleasePlanError` naming the supported keys, and the engine's formatter integration is disabled so the caller owns formatting.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const preview = yield* planner.preview(root, {
|
|
290
|
+
changelogModules: { "@savvy-web/changelog": changelogModulePath },
|
|
291
|
+
});
|
|
292
|
+
// => ChangesetPreview: per-release changelogEntry, versions and changeset ids
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`Changesets.ReleasePlannerLive` requires `ConfigInspector` and `FileSystem`.
|
|
296
|
+
|
|
261
297
|
---
|
|
262
298
|
|
|
263
299
|
### FileSystem + process layer required
|
|
264
300
|
|
|
301
|
+
#### BranchAnalyzer
|
|
302
|
+
|
|
303
|
+
`analyzeBranch` classifies a branch's diff by the package that owns each file, applying `ConfigInspector` attribution over the git range. This is what answers "what changed on this branch, and which package releases because of it", including the unmapped files that belong to no package.
|
|
304
|
+
|
|
305
|
+
`Changesets.BranchAnalyzerLive` requires `ConfigInspector` and the platform process spawner.
|
|
306
|
+
|
|
307
|
+
#### DepsRegen
|
|
308
|
+
|
|
309
|
+
Own dependency-changeset orchestration, split so that detection and regeneration share one code path: `plan(options)` computes a complete, side-effect-free `RegenPlan` — target filenames, each row's from/to version, and any stale pure-dependency changesets marked for deletion — and `execute(plan)` applies exactly what the plan describes. A dry run is `plan()` plus rendering.
|
|
310
|
+
|
|
311
|
+
Both sides of the diff are snapshotted at their own git ref, so `catalog:` and `workspace:` specifiers resolve per side before the comparison. A specifier that changes protocol without changing its resolved version produces no row.
|
|
312
|
+
|
|
313
|
+
`Changesets.DepsRegenDefault` is the batteries-included layer, composing the full graph with silk's opinionated defaults and leaving only the platform services open. Because snapshots read git history, provide a spawn-capable layer such as `NodeServices.layer` rather than a filesystem-only one.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { Effect } from "effect";
|
|
317
|
+
import { NodeServices } from "@effect/platform-node";
|
|
318
|
+
import { Changesets } from "@savvy-web/silk-effects";
|
|
319
|
+
|
|
320
|
+
const plan = await Effect.runPromise(
|
|
321
|
+
Effect.gen(function* () {
|
|
322
|
+
const regen = yield* Changesets.DepsRegen;
|
|
323
|
+
return yield* regen.plan({});
|
|
324
|
+
}).pipe(
|
|
325
|
+
Effect.provide(Changesets.DepsRegenDefault),
|
|
326
|
+
Effect.provide(NodeServices.layer),
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
// => RegenPlan: files to write, rows per package, changesets to delete
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`Changesets.DepsRegenLive` is the seam for callers injecting their own dependencies; it requires `WorkspaceSnapshots`, `ConfigInspector`, `WorkspaceDiscovery`, `PublishabilityDetector`, `ChangesetConfig`, `Git` and `FileSystem`.
|
|
333
|
+
|
|
265
334
|
#### TurboInspector
|
|
266
335
|
|
|
267
336
|
Read-only Turborepo inspection. Every method shells out to `turbo` with `--dry=json`, so no task ever runs. `diagnoseCache(task, cwd)` reports a per-package cache HIT/MISS breakdown for a task, `taskGraph(cwd, task?)` derives the task graph and its critical path and `affected(cwd, base?)` lists the packages affected relative to `base` (default `main`). It resolves the `turbo` binary through `ToolDiscovery` from [`@effected/commands`](https://www.npmjs.com/package/@effected/commands) and fails with a tagged error when `turbo` is missing or the directory is not a Turborepo. The service tag and its layer are exported under the `Turbo` namespace.
|
|
@@ -49,7 +49,7 @@ function makeShape(inspector, fs) {
|
|
|
49
49
|
reason: errMsg(e)
|
|
50
50
|
})
|
|
51
51
|
});
|
|
52
|
-
const preview = (root) => previewEffect(root, fs);
|
|
52
|
+
const preview = (root, options) => previewEffect(root, options?.changelogModules, fs);
|
|
53
53
|
const apply = (root, options) => applyEffect(root, options?.dryRun ?? false, options?.changelogModules, inspector, fs);
|
|
54
54
|
return {
|
|
55
55
|
plan,
|
|
@@ -78,6 +78,44 @@ function makeReleasePlannerTest(fixed) {
|
|
|
78
78
|
apply: () => fixed.apply ? Effect.succeed(fixed.apply) : fail("apply")
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Rewrite the engine config for a caller that supplies its own changelog module
|
|
83
|
+
* paths: `config.changelog[0]` becomes the mapped absolute path and the engine's
|
|
84
|
+
* `format` integration is switched off.
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* Shared by `preview` and `apply` — both hand the result to
|
|
88
|
+
* `applyReleasePlan`, and both are called from no-`node_modules` contexts where
|
|
89
|
+
* neither the configured id nor a formatter can be resolved. An unmapped id
|
|
90
|
+
* fails typed (naming the supported keys) rather than reaching
|
|
91
|
+
* `import-meta-resolve`, which reports it as `expected to be defined`.
|
|
92
|
+
*
|
|
93
|
+
* Exported for tests only — not re-exported from the package index, so it is
|
|
94
|
+
* not public API. Driving it directly is the only deterministic way to assert
|
|
95
|
+
* the `format: false` half: every formatter the changesets config accepts
|
|
96
|
+
* either shells out through `npx` (network) or to an ambient binary, so a
|
|
97
|
+
* fixture that names one passes or fails on what the machine happens to have
|
|
98
|
+
* installed rather than on this rewrite.
|
|
99
|
+
*/
|
|
100
|
+
function withChangelogModules(config, changelogModules, phase) {
|
|
101
|
+
const unformatted = {
|
|
102
|
+
...config,
|
|
103
|
+
format: false
|
|
104
|
+
};
|
|
105
|
+
if (!Array.isArray(config.changelog)) return Effect.succeed(unformatted);
|
|
106
|
+
const configuredId = config.changelog[0];
|
|
107
|
+
if (!Object.hasOwn(changelogModules, configuredId)) {
|
|
108
|
+
const supported = Object.keys(changelogModules).join(", ");
|
|
109
|
+
return Effect.fail(new ReleasePlanError({
|
|
110
|
+
phase,
|
|
111
|
+
reason: `changelog id "${configuredId}" is not in changelogModules (supported: ${supported})`
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
return Effect.succeed({
|
|
115
|
+
...unformatted,
|
|
116
|
+
changelog: [changelogModules[configuredId], config.changelog[1]]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
81
119
|
/** Extract the `## <version>` block (down to the next H2 or EOF) from a changelog. */
|
|
82
120
|
function extractVersionBlock(changelog, version) {
|
|
83
121
|
const lines = changelog.split("\n");
|
|
@@ -105,7 +143,7 @@ function maintenanceReasons(plan, config) {
|
|
|
105
143
|
* scope-managed temp directory (cleaned up automatically when the scope
|
|
106
144
|
* closes) and reading the generated CHANGELOG blocks back.
|
|
107
145
|
*/
|
|
108
|
-
function previewEffect(root, fs) {
|
|
146
|
+
function previewEffect(root, changelogModules, fs) {
|
|
109
147
|
const program = Effect.gen(function* () {
|
|
110
148
|
const [plan, packages] = yield* Effect.tryPromise({
|
|
111
149
|
try: () => Promise.all([getReleasePlan(root), getPackages(root)]),
|
|
@@ -128,6 +166,7 @@ function previewEffect(root, fs) {
|
|
|
128
166
|
});
|
|
129
167
|
yield* Effect.forEach(warnings, (w) => Effect.logWarning(w));
|
|
130
168
|
const reasonByName = maintenanceReasons(plan, config);
|
|
169
|
+
const engineConfig = changelogModules ? yield* withChangelogModules(config, changelogModules, "preview") : config;
|
|
131
170
|
const preMode = plan.preState ? plan.preState.mode : null;
|
|
132
171
|
const changesets = plan.changesets.map((cs) => ({
|
|
133
172
|
id: cs.id,
|
|
@@ -180,7 +219,7 @@ function previewEffect(root, fs) {
|
|
|
180
219
|
const rootCl = join(packages.rootDir, "CHANGELOG.md");
|
|
181
220
|
if (yield* fs.exists(rootCl)) yield* fs.copyFile(rootCl, join(tempRoot, "CHANGELOG.md"));
|
|
182
221
|
yield* Effect.tryPromise({
|
|
183
|
-
try: () => applyReleasePlan(plan, tempPackages,
|
|
222
|
+
try: () => applyReleasePlan(plan, tempPackages, engineConfig, void 0, root),
|
|
184
223
|
catch: (e) => new ReleasePlanError({
|
|
185
224
|
phase: "preview",
|
|
186
225
|
reason: errMsg(e)
|
|
@@ -250,28 +289,7 @@ function applyEffect(root, dryRun, changelogModules, inspector, fs) {
|
|
|
250
289
|
})
|
|
251
290
|
});
|
|
252
291
|
yield* Effect.forEach(warnings, (w) => Effect.logWarning(w));
|
|
253
|
-
|
|
254
|
-
if (changelogModules) {
|
|
255
|
-
engineConfig = {
|
|
256
|
-
...config,
|
|
257
|
-
format: false
|
|
258
|
-
};
|
|
259
|
-
if (Array.isArray(config.changelog)) {
|
|
260
|
-
const configuredId = config.changelog[0];
|
|
261
|
-
const mapped = changelogModules[configuredId];
|
|
262
|
-
if (mapped === void 0) {
|
|
263
|
-
const supported = Object.keys(changelogModules).join(", ");
|
|
264
|
-
return yield* Effect.fail(new ReleasePlanError({
|
|
265
|
-
phase: "apply",
|
|
266
|
-
reason: `changelog id "${configuredId}" is not in changelogModules (supported: ${supported})`
|
|
267
|
-
}));
|
|
268
|
-
}
|
|
269
|
-
engineConfig = {
|
|
270
|
-
...engineConfig,
|
|
271
|
-
changelog: [mapped, config.changelog[1]]
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
}
|
|
292
|
+
const engineConfig = changelogModules ? yield* withChangelogModules(config, changelogModules, "apply") : config;
|
|
275
293
|
const releases = plan.releases.filter((r) => r.type !== "none").map((r) => ({
|
|
276
294
|
name: r.name,
|
|
277
295
|
type: r.type,
|
|
@@ -337,4 +355,4 @@ function applyEffect(root, dryRun, changelogModules, inspector, fs) {
|
|
|
337
355
|
}
|
|
338
356
|
|
|
339
357
|
//#endregion
|
|
340
|
-
export { ReleasePlanner, ReleasePlannerLive, extractVersionBlock, makeReleasePlannerTest };
|
|
358
|
+
export { ReleasePlanner, ReleasePlannerLive, extractVersionBlock, makeReleasePlannerTest, withChangelogModules };
|
package/index.d.ts
CHANGED
|
@@ -3899,8 +3899,22 @@ type AppliedRelease = Schema.Schema.Type<typeof AppliedReleaseSchema>;
|
|
|
3899
3899
|
interface ReleasePlannerShape {
|
|
3900
3900
|
/** Compute the in-memory release plan (read-only). */
|
|
3901
3901
|
readonly plan: (root: string) => Effect.Effect<ReleasePlan, ReleasePlanError>;
|
|
3902
|
-
/**
|
|
3903
|
-
|
|
3902
|
+
/**
|
|
3903
|
+
* Render a non-destructive preview of the next release.
|
|
3904
|
+
*
|
|
3905
|
+
* @remarks
|
|
3906
|
+
* Rendering `changelogEntry` means resolving the configured changelog module,
|
|
3907
|
+
* so `changelogModules` matters here for the same reason it does on `apply`.
|
|
3908
|
+
*/
|
|
3909
|
+
readonly preview: (root: string, options?: {
|
|
3910
|
+
/**
|
|
3911
|
+
* Map configured changelog ids to absolute module paths. When set,
|
|
3912
|
+
* `config.changelog[0]` must be a key of this map (rewritten before the
|
|
3913
|
+
* engine call; unmapped ids fail) and the engine's `format` integration
|
|
3914
|
+
* is disabled — callers in no-`node_modules` contexts own formatting.
|
|
3915
|
+
*/
|
|
3916
|
+
readonly changelogModules?: Readonly<Record<string, string>>;
|
|
3917
|
+
}) => Effect.Effect<ChangesetPreview, ReleasePlanError>;
|
|
3904
3918
|
/** Natively apply the release (destructive unless `dryRun`). */
|
|
3905
3919
|
readonly apply: (root: string, options?: {
|
|
3906
3920
|
readonly dryRun?: boolean;
|
package/package.json
CHANGED