@savvy-web/silk-effects 7.0.1 → 7.1.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.
- package/README.md +4 -1
- package/changesets/api/linter.js +1 -1
- package/changesets/api/transformer.js +10 -7
- package/changesets/changelog/getReleaseLine.js +114 -20
- package/changesets/changelog/vanilla.js +47 -0
- package/changesets/constants.js +7 -2
- package/changesets/index.js +7 -4
- package/changesets/markdownlint/rules/content-structure.js +2 -2
- package/changesets/markdownlint/rules/dependency-table-format.js +12 -11
- package/changesets/markdownlint/rules/heading-hierarchy.js +2 -2
- package/changesets/markdownlint/rules/required-sections.js +2 -2
- package/changesets/markdownlint/rules/uncategorized-content.js +3 -3
- package/changesets/markdownlint/rules/utils.js +17 -6
- package/changesets/remark/plugins/aggregate-dependency-tables.js +53 -1
- package/changesets/remark/plugins/contributor-footnotes.js +276 -64
- package/changesets/remark/plugins/reorder-sections.js +18 -3
- package/changesets/remark/presets.js +1 -1
- package/changesets/remark/rules/dependency-table-format.js +7 -9
- package/changesets/schemas/dependency-table.js +11 -3
- package/changesets/schemas/options.js +8 -0
- package/changesets/services/config-inspector.js +51 -4
- package/changesets/services/deps-regen.js +106 -10
- package/changesets/services/release-planner.js +29 -8
- package/changesets/utils/dep-diff.js +34 -5
- package/changesets/utils/dependency-section.js +34 -0
- package/changesets/utils/dependency-table.js +14 -9
- package/changesets/utils/markdown-emit.js +86 -0
- package/changesets/utils/remark-pipeline.js +14 -85
- package/changesets/utils/section-parser.js +4 -2
- package/index.d.ts +158 -24
- package/index.js +1 -1
- package/lint/index.js +1 -1
- package/package.json +4 -2
|
@@ -201,6 +201,85 @@ const findPureDependencyChangesets = (fs, changesetDir) => Effect.gen(function*
|
|
|
201
201
|
return result;
|
|
202
202
|
});
|
|
203
203
|
/**
|
|
204
|
+
* Every package name a changeset's YAML frontmatter releases, parsed with the
|
|
205
|
+
* same lenient `"@pkg": bump` line grammar {@link isPureDependencyChangeset}
|
|
206
|
+
* uses. A file with no frontmatter, or one whose lines all fail the grammar,
|
|
207
|
+
* yields an empty list — this feeds an informational surface, so lenient
|
|
208
|
+
* degradation beats a typed failure.
|
|
209
|
+
*
|
|
210
|
+
* @param content - Raw `.changeset/*.md` file contents.
|
|
211
|
+
*
|
|
212
|
+
* @public
|
|
213
|
+
*/
|
|
214
|
+
function parseChangesetPackages(content) {
|
|
215
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
216
|
+
if (!fmMatch) return [];
|
|
217
|
+
const packages = [];
|
|
218
|
+
for (const line of fmMatch[1].split(/\r?\n/)) {
|
|
219
|
+
if (line.trim().length === 0 || /^\s*#/.test(line)) continue;
|
|
220
|
+
const m = line.match(/^\s*["']?([^"':\s]+)["']?\s*:\s*([a-z]+)\s*$/);
|
|
221
|
+
if (m) packages.push(m[1]);
|
|
222
|
+
}
|
|
223
|
+
return packages;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Whether `content` carries a `## Dependencies` heading OUTSIDE fenced code
|
|
227
|
+
* blocks. A prose changeset that DOCUMENTS the changeset format quotes the
|
|
228
|
+
* heading inside a ``` fence; a fence-blind regex misreads that file as a
|
|
229
|
+
* dependency changeset (classified mixed, dropped from the coexisting
|
|
230
|
+
* bucket). Tracks CommonMark fence state: an opening run of 3+ backticks or
|
|
231
|
+
* tildes (a backtick fence's info string may not contain a backtick) is
|
|
232
|
+
* closed only by a run of the same character, at least as long, with nothing
|
|
233
|
+
* else on the line.
|
|
234
|
+
*/
|
|
235
|
+
function containsDependenciesHeading(content) {
|
|
236
|
+
let fence = null;
|
|
237
|
+
for (const line of content.split(/\r?\n/)) {
|
|
238
|
+
const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
239
|
+
if (match) {
|
|
240
|
+
const run = match[1];
|
|
241
|
+
const char = run.charAt(0);
|
|
242
|
+
const trailing = match[2];
|
|
243
|
+
if (fence === null) {
|
|
244
|
+
if (char === "~" || !trailing.includes("`")) {
|
|
245
|
+
fence = {
|
|
246
|
+
char,
|
|
247
|
+
length: run.length
|
|
248
|
+
};
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
} else if (char === fence.char && run.length >= fence.length && trailing.trim() === "") {
|
|
252
|
+
fence = null;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (fence === null && /^## Dependencies\b/.test(line)) return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Prose-only changesets (no `## Dependencies` heading at all) found in
|
|
262
|
+
* `changesetDir`, each with the packages its frontmatter releases. These are
|
|
263
|
+
* the files a regen run never touches — surfaced so the result can account
|
|
264
|
+
* for them instead of leaving them invisible (#279). Same
|
|
265
|
+
* skip-unreadable-file / loud-list-failure semantics as
|
|
266
|
+
* {@link findPureDependencyChangesets}.
|
|
267
|
+
*/
|
|
268
|
+
const findProseChangesets = (fs, changesetDir) => Effect.gen(function* () {
|
|
269
|
+
const files = yield* listChangesetFiles(fs, changesetDir);
|
|
270
|
+
const result = [];
|
|
271
|
+
for (const file of files) {
|
|
272
|
+
const content = yield* fs.readFileString(file).pipe(Effect.option);
|
|
273
|
+
if (Option.isNone(content)) continue;
|
|
274
|
+
if (containsDependenciesHeading(content.value)) continue;
|
|
275
|
+
result.push({
|
|
276
|
+
file,
|
|
277
|
+
packages: parseChangesetPackages(content.value)
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
});
|
|
282
|
+
/**
|
|
204
283
|
* Mixed dependency changesets (have a `## Dependencies` heading but fail
|
|
205
284
|
* the strict pure-changeset test) found in `changesetDir`. Same
|
|
206
285
|
* skip-unreadable-file / loud-list-failure semantics as
|
|
@@ -212,7 +291,7 @@ const findMixedDependencyChangesets = (fs, changesetDir) => Effect.gen(function*
|
|
|
212
291
|
for (const file of files) {
|
|
213
292
|
const content = yield* fs.readFileString(file).pipe(Effect.option);
|
|
214
293
|
if (Option.isNone(content)) continue;
|
|
215
|
-
if (
|
|
294
|
+
if (containsDependenciesHeading(content.value) && !isPureDependencyChangeset(content.value).isPure) result.push(file);
|
|
216
295
|
}
|
|
217
296
|
return result;
|
|
218
297
|
});
|
|
@@ -305,10 +384,15 @@ function makeShape(snapshots, inspector, discovery, detector, config, fs, provid
|
|
|
305
384
|
}
|
|
306
385
|
const inScopeFor = (name) => explicitTargets.size > 0 ? activeTargets.has(name) : inScope.has(name) && !excluded.has(name);
|
|
307
386
|
const keepDevDeps = options.includeDevDeps === true;
|
|
387
|
+
const releaseNeutral = /* @__PURE__ */ new Set([
|
|
388
|
+
"devDependency",
|
|
389
|
+
"runtime",
|
|
390
|
+
"packageManager"
|
|
391
|
+
]);
|
|
308
392
|
const scoped = rawDiffs.filter((d) => inScopeFor(d.package));
|
|
309
393
|
const resolved = [];
|
|
310
394
|
for (const diff of scoped) {
|
|
311
|
-
const rows = keepDevDeps ? [...diff.rows] : diff.rows.filter((r) => r.type
|
|
395
|
+
const rows = keepDevDeps ? [...diff.rows] : diff.rows.filter((r) => !releaseNeutral.has(r.type));
|
|
312
396
|
if (rows.length > 0) resolved.push({
|
|
313
397
|
...diff,
|
|
314
398
|
rows: sortDependencyRows(rows)
|
|
@@ -316,6 +400,10 @@ function makeShape(snapshots, inspector, discovery, detector, config, fs, provid
|
|
|
316
400
|
}
|
|
317
401
|
const existingPure = yield* findPureDependencyChangesets(fs, changesetDir);
|
|
318
402
|
const skippedMixed = yield* findMixedDependencyChangesets(fs, changesetDir);
|
|
403
|
+
const coexisting = (yield* findProseChangesets(fs, changesetDir)).map((p) => ({
|
|
404
|
+
file: p.file,
|
|
405
|
+
packages: p.packages.filter(inScopeFor)
|
|
406
|
+
})).filter((p) => p.packages.length > 0);
|
|
319
407
|
const rewrittenPackages = new Set(resolved.map((d) => d.package));
|
|
320
408
|
const atMergeBase = yield* gitListChangesetFilesAtRef(resolvedCwd, fromRef).pipe(Effect.provide(provideGit));
|
|
321
409
|
const authoredOnBranch = (file) => !atMergeBase.has(basename(file));
|
|
@@ -333,7 +421,8 @@ function makeShape(snapshots, inspector, discovery, detector, config, fs, provid
|
|
|
333
421
|
return {
|
|
334
422
|
toDelete,
|
|
335
423
|
toWrite,
|
|
336
|
-
skippedMixed
|
|
424
|
+
skippedMixed,
|
|
425
|
+
coexisting
|
|
337
426
|
};
|
|
338
427
|
});
|
|
339
428
|
const execute = (plan) => Effect.gen(function* () {
|
|
@@ -351,7 +440,8 @@ function makeShape(snapshots, inspector, discovery, detector, config, fs, provid
|
|
|
351
440
|
return {
|
|
352
441
|
deleted,
|
|
353
442
|
written,
|
|
354
|
-
skippedMixed: plan.skippedMixed
|
|
443
|
+
skippedMixed: plan.skippedMixed,
|
|
444
|
+
coexisting: plan.coexisting
|
|
355
445
|
};
|
|
356
446
|
});
|
|
357
447
|
return {
|
|
@@ -372,15 +462,21 @@ const ConfigGraph = ChangesetConfig.layer.pipe(Layer.provide(ChangesetConfigRead
|
|
|
372
462
|
* lazily); pass an explicit `cwd` when planning against a different root
|
|
373
463
|
* (fixtures, multi-repo hosts).
|
|
374
464
|
*
|
|
375
|
-
* `Workspaces.
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
465
|
+
* The graph is `Workspaces.layerWithGitAndConfigDependenciesSubprocess` —
|
|
466
|
+
* `layerWithGit`'s service set with config-dependency hook replay in catalog
|
|
467
|
+
* assembly, so hook-injected catalogs resolve to ranges (#539). The subprocess
|
|
468
|
+
* replay is chosen over the in-process one because silk's consumers (the
|
|
469
|
+
* `savvy` CLI, the `savvy-mcp` server) are bundled, and a bundler compiles the
|
|
470
|
+
* in-process replay's computed dynamic `import()` into a context module that
|
|
471
|
+
* cannot resolve at runtime. The kit mints a fresh layer reference per call,
|
|
472
|
+
* so the graph is bound ONCE per builder call and shared across every internal
|
|
473
|
+
* branch — layer memoization by reference constructs each kit service exactly
|
|
474
|
+
* once.
|
|
379
475
|
*
|
|
380
476
|
* @public
|
|
381
477
|
*/
|
|
382
478
|
function makeDepsRegenDefault(options) {
|
|
383
|
-
const kitGraph = Workspaces.
|
|
479
|
+
const kitGraph = Workspaces.layerWithGitAndConfigDependenciesSubprocess(options);
|
|
384
480
|
return DepsRegen.layer.pipe(Layer.provide(ConfigInspector.layer.pipe(Layer.provide(Layer.mergeAll(ChangesetConfigReader.layer, kitGraph)))), Layer.provide(SilkPublishability.layerAdaptive.pipe(Layer.provide(Layer.mergeAll(ConfigGraph, kitGraph)))), Layer.provide(ConfigGraph), Layer.provide(kitGraph));
|
|
385
481
|
}
|
|
386
482
|
/**
|
|
@@ -413,4 +509,4 @@ function makeDepsRegenDefault(options) {
|
|
|
413
509
|
const DepsRegenDefault = makeDepsRegenDefault();
|
|
414
510
|
|
|
415
511
|
//#endregion
|
|
416
|
-
export { DepsRegen, DepsRegenDefault, isPureDependencyChangeset, makeDepsRegenDefault };
|
|
512
|
+
export { DepsRegen, DepsRegenDefault, isPureDependencyChangeset, makeDepsRegenDefault, parseChangesetPackages };
|
|
@@ -129,6 +129,19 @@ function extractVersionBlock(changelog, version) {
|
|
|
129
129
|
}
|
|
130
130
|
return lines.slice(start, end).join("\n").trim();
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Read the `thanks` changelog option from the changesets config tuple
|
|
134
|
+
* (`"changelog": ["@savvy-web/changelog", { "thanks": false }]`), if set.
|
|
135
|
+
* A missing or non-boolean value returns `undefined` so the transformer's
|
|
136
|
+
* default (`true`) applies.
|
|
137
|
+
*/
|
|
138
|
+
function thanksOption(config) {
|
|
139
|
+
if (!Array.isArray(config.changelog)) return void 0;
|
|
140
|
+
const opts = config.changelog[1];
|
|
141
|
+
if (typeof opts !== "object" || opts === null) return void 0;
|
|
142
|
+
const thanks = opts.thanks;
|
|
143
|
+
return typeof thanks === "boolean" ? thanks : void 0;
|
|
144
|
+
}
|
|
132
145
|
/** Maintenance reasons for every changeset-less release in the plan, keyed by package name. */
|
|
133
146
|
function maintenanceReasons(plan, config) {
|
|
134
147
|
const reasons = /* @__PURE__ */ new Map();
|
|
@@ -236,11 +249,15 @@ function previewEffect(root, changelogModules, fs) {
|
|
|
236
249
|
const clPath = join(dir, "CHANGELOG.md");
|
|
237
250
|
if (!(yield* fs.exists(clPath))) continue;
|
|
238
251
|
const reason = reasonByName.get(r.name);
|
|
252
|
+
const thanks = thanksOption(config);
|
|
239
253
|
yield* Effect.try({
|
|
240
|
-
try: () => ChangelogTransformer.transformFile(clPath,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
254
|
+
try: () => ChangelogTransformer.transformFile(clPath, {
|
|
255
|
+
...reason ? { maintenance: {
|
|
256
|
+
version: r.newVersion,
|
|
257
|
+
reason
|
|
258
|
+
} } : {},
|
|
259
|
+
...thanks !== void 0 ? { thanks } : {}
|
|
260
|
+
}),
|
|
244
261
|
catch: (e) => new ReleasePlanError({
|
|
245
262
|
phase: "preview",
|
|
246
263
|
reason: errMsg(e)
|
|
@@ -300,6 +317,7 @@ function applyEffect(root, dryRun, changelogModules, inspector, fs) {
|
|
|
300
317
|
let touchedFiles = [];
|
|
301
318
|
if (!dryRun) {
|
|
302
319
|
const reasonByName = maintenanceReasons(plan, config);
|
|
320
|
+
const thanks = thanksOption(config);
|
|
303
321
|
const versionByPkgName = new Map(plan.releases.map((r) => [r.name, r.newVersion]));
|
|
304
322
|
const nameByDir = /* @__PURE__ */ new Map();
|
|
305
323
|
for (const p of packages.packages) nameByDir.set(p.dir, p.packageJson.name);
|
|
@@ -312,10 +330,13 @@ function applyEffect(root, dryRun, changelogModules, inspector, fs) {
|
|
|
312
330
|
const pkgName = nameByDir.get(dirname(f));
|
|
313
331
|
const reason = pkgName ? reasonByName.get(pkgName) : void 0;
|
|
314
332
|
const newVersion = pkgName ? versionByPkgName.get(pkgName) : void 0;
|
|
315
|
-
ChangelogTransformer.transformFile(f,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
ChangelogTransformer.transformFile(f, {
|
|
334
|
+
...reason && newVersion ? { maintenance: {
|
|
335
|
+
version: newVersion,
|
|
336
|
+
reason
|
|
337
|
+
} } : {},
|
|
338
|
+
...thanks !== void 0 ? { thanks } : {}
|
|
339
|
+
});
|
|
319
340
|
}
|
|
320
341
|
return touched;
|
|
321
342
|
},
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
import { sortDependencyRows } from "./dependency-table.js";
|
|
2
2
|
import { Option } from "effect";
|
|
3
|
+
import { WorkspaceStateSnapshot } from "@effected/workspaces";
|
|
3
4
|
|
|
4
5
|
//#region src/changesets/utils/dep-diff.ts
|
|
6
|
+
/**
|
|
7
|
+
* Compute per-workspace-package dependency-table rows from two
|
|
8
|
+
* {@link WorkspaceStateSnapshot}s, resolving `catalog:` / `workspace:`
|
|
9
|
+
* specifiers against each side's own catalogs and package versions BEFORE
|
|
10
|
+
* comparing.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Operates on declared dependencies only (the `dependencies` /
|
|
14
|
+
* `devDependencies` / `peerDependencies` / `optionalDependencies` fields
|
|
15
|
+
* of each workspace's `package.json`). Lockfile-only movements
|
|
16
|
+
* (resolved versions changing while declared ranges stay put) are
|
|
17
|
+
* intentionally excluded — those happen on every `pnpm install` and
|
|
18
|
+
* would generate constant noise.
|
|
19
|
+
*
|
|
20
|
+
* Each side carries its own catalogs and package versions, so a specifier
|
|
21
|
+
* is resolved against the snapshot it belongs to: `catalog:silk` resolves
|
|
22
|
+
* to that ref's `silk` catalog entry, `workspace:*` to that ref's target
|
|
23
|
+
* package version. A row is emitted iff the two RESOLVED values differ (or
|
|
24
|
+
* the dependency was added/removed) — a package that merely adopted a
|
|
25
|
+
* `catalog:` specifier without changing the concrete version produces NO
|
|
26
|
+
* row. When a side cannot resolve a specifier (no matching catalog entry,
|
|
27
|
+
* plain range, etc.) it falls back to the raw specifier string.
|
|
28
|
+
*
|
|
29
|
+
* @see {@link DependencyTableRow} for the row schema
|
|
30
|
+
* @see {@link WorkspaceStateSnapshot} for the input shape
|
|
31
|
+
*
|
|
32
|
+
*/
|
|
5
33
|
/** The em-dash sentinel (U+2014) used for added ("from") / removed ("to") cells. */
|
|
6
34
|
const EM_DASH = "—";
|
|
7
35
|
const DEP_TYPE_MAP = [
|
|
@@ -69,8 +97,9 @@ const collapseFieldMoves = (rows) => {
|
|
|
69
97
|
*/
|
|
70
98
|
function computeWorkspaceDependencyDiffs(before, after) {
|
|
71
99
|
const result = [];
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
const [seededBefore, seededAfter] = WorkspaceStateSnapshot.crossSeed(before, after);
|
|
101
|
+
for (const afterPkg of seededAfter.packages) {
|
|
102
|
+
const beforePkg = Option.getOrNull(seededBefore.package(afterPkg.name));
|
|
74
103
|
const rows = [];
|
|
75
104
|
const afterImporter = afterPkg.relativePath;
|
|
76
105
|
const beforeImporter = beforePkg?.relativePath ?? afterImporter;
|
|
@@ -80,7 +109,7 @@ function computeWorkspaceDependencyDiffs(before, after) {
|
|
|
80
109
|
const seen = /* @__PURE__ */ new Set();
|
|
81
110
|
for (const [name, beforeSpec] of Object.entries(beforeRecord)) {
|
|
82
111
|
seen.add(name);
|
|
83
|
-
const from = resolveOrRaw(
|
|
112
|
+
const from = resolveOrRaw(seededBefore, beforeImporter, name, beforeSpec);
|
|
84
113
|
const afterSpec = afterRecord[name];
|
|
85
114
|
if (afterSpec === void 0) {
|
|
86
115
|
rows.push({
|
|
@@ -92,7 +121,7 @@ function computeWorkspaceDependencyDiffs(before, after) {
|
|
|
92
121
|
});
|
|
93
122
|
continue;
|
|
94
123
|
}
|
|
95
|
-
const to = resolveOrRaw(
|
|
124
|
+
const to = resolveOrRaw(seededAfter, afterImporter, name, afterSpec);
|
|
96
125
|
if (from !== to) rows.push({
|
|
97
126
|
dependency: name,
|
|
98
127
|
type,
|
|
@@ -108,7 +137,7 @@ function computeWorkspaceDependencyDiffs(before, after) {
|
|
|
108
137
|
type,
|
|
109
138
|
action: "added",
|
|
110
139
|
from: EM_DASH,
|
|
111
|
-
to: resolveOrRaw(
|
|
140
|
+
to: resolveOrRaw(seededAfter, afterImporter, name, afterSpec)
|
|
112
141
|
});
|
|
113
142
|
}
|
|
114
143
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/changesets/utils/dependency-section.ts
|
|
2
|
+
/**
|
|
3
|
+
* Scan a `## Dependencies` section starting at the node AFTER its heading.
|
|
4
|
+
*
|
|
5
|
+
* Collects every content block until the next heading (any depth) or end of
|
|
6
|
+
* input, and selects the first table found anywhere among them. See the module
|
|
7
|
+
* remarks for the semantics this encodes and why they live in one place.
|
|
8
|
+
*
|
|
9
|
+
* @param nodes - The sibling node list containing the section (root children
|
|
10
|
+
* for mdast, the top-level token list for micromark)
|
|
11
|
+
* @param startIndex - Index of the first node after the section heading
|
|
12
|
+
* @param adapter - Engine-specific node predicates
|
|
13
|
+
* @returns The section's blocks and its first table, if any
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
function scanDependencySection(nodes, startIndex, adapter) {
|
|
18
|
+
const blocks = [];
|
|
19
|
+
let table;
|
|
20
|
+
for (let i = startIndex; i < nodes.length; i++) {
|
|
21
|
+
const node = nodes[i];
|
|
22
|
+
if (adapter.isSkippable?.(node)) continue;
|
|
23
|
+
if (adapter.isHeading(node)) break;
|
|
24
|
+
blocks.push(node);
|
|
25
|
+
if (table === void 0 && adapter.isTable(node)) table = node;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
blocks,
|
|
29
|
+
table
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { scanDependencySection };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DependencyTableRowSchema } from "../schemas/dependency-table.js";
|
|
2
|
-
import {
|
|
2
|
+
import { stringifyMarkdown } from "./remark-pipeline.js";
|
|
3
3
|
import { Schema } from "effect";
|
|
4
4
|
import { toString } from "mdast-util-to-string";
|
|
5
5
|
|
|
@@ -101,10 +101,12 @@ function parseDependencyTable(table) {
|
|
|
101
101
|
* Create a table cell with a text node.
|
|
102
102
|
*
|
|
103
103
|
* @remarks
|
|
104
|
-
* The cell
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
104
|
+
* The cell holds a plain text node; the canonical stringifier escapes any
|
|
105
|
+
* character that could open a markdown construct (`~0.2.1` is written as
|
|
106
|
+
* `\~0.2.1`, `some_pkg` as `some\_pkg`, `|` as `\|`). Parsing consumes the
|
|
107
|
+
* escapes, so cell VALUES round-trip byte-identically through
|
|
108
|
+
* parse-and-reserialize — the escaping is a raw-byte spelling, never a
|
|
109
|
+
* value change, and it cannot compound across cycles.
|
|
108
110
|
*
|
|
109
111
|
* @param text - The cell text content
|
|
110
112
|
* @returns An MDAST `TableCell` node
|
|
@@ -114,7 +116,10 @@ function parseDependencyTable(table) {
|
|
|
114
116
|
function makeCell(text) {
|
|
115
117
|
return {
|
|
116
118
|
type: "tableCell",
|
|
117
|
-
children: [
|
|
119
|
+
children: [{
|
|
120
|
+
type: "text",
|
|
121
|
+
value: text
|
|
122
|
+
}]
|
|
118
123
|
};
|
|
119
124
|
}
|
|
120
125
|
/**
|
|
@@ -167,9 +172,9 @@ function serializeDependencyTable(rows) {
|
|
|
167
172
|
* Serialize dependency table rows to a markdown table string.
|
|
168
173
|
*
|
|
169
174
|
* @remarks
|
|
170
|
-
* Combines {@link serializeDependencyTable} with
|
|
171
|
-
* to produce a ready-to-use GFM markdown
|
|
172
|
-
* of leading/trailing whitespace.
|
|
175
|
+
* Combines {@link serializeDependencyTable} with the canonical
|
|
176
|
+
* `@effected/markdown` emit boundary to produce a ready-to-use GFM markdown
|
|
177
|
+
* table string. The result is trimmed of leading/trailing whitespace.
|
|
173
178
|
*
|
|
174
179
|
* @param rows - Array of `DependencyTableRow` objects
|
|
175
180
|
* @returns Markdown table string (GFM format)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
import { Markdown, Mdast } from "@effected/markdown";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/utils/markdown-emit.ts
|
|
5
|
+
/**
|
|
6
|
+
* Unwrap a `Result` from the kit's emit boundary, converting a typed failure
|
|
7
|
+
* into a defect.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The trees this pipeline emits are either parsed by our own remark pipeline
|
|
11
|
+
* or synthesized by our own plugins, so a decode or stringify failure is a
|
|
12
|
+
* programmer error, not an operational condition — the sync call sites treat
|
|
13
|
+
* it as a defect with the typed error preserved as `cause`.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
function unwrap(result, context) {
|
|
18
|
+
if (Result.isFailure(result)) {
|
|
19
|
+
const error = result.failure;
|
|
20
|
+
throw new Error(`${context}: ${error.message}`, { cause: error });
|
|
21
|
+
}
|
|
22
|
+
return result.success;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Default `fenceChar` on language-less code nodes so they emit FENCED.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* The kit's canonical form emits a code node carrying neither `lang` nor
|
|
29
|
+
* `fenceChar` as an INDENTED block — and `lang: null` is exactly what
|
|
30
|
+
* remark-parse produces for a ``` fence with no info string, so without this
|
|
31
|
+
* pass every language-less snippet in changelog content would change shape.
|
|
32
|
+
* Worse, the indented default is neighbor-dependent: the serializer takes a
|
|
33
|
+
* representability escape back to a backtick fence when the indented form
|
|
34
|
+
* would misparse (e.g. code immediately after a list), so the same node
|
|
35
|
+
* spells differently depending on what precedes it — inconsistent raw
|
|
36
|
+
* output. House policy is fenced code everywhere, so the ONE emit boundary defaults
|
|
37
|
+
* the kit's own per-node fidelity field, `fenceChar`, post-decode. It has to
|
|
38
|
+
* happen post-decode: `Mdast.fromMdast` drops non-mdast fields from foreign
|
|
39
|
+
* trees, so a `fenceChar` set on the plain tree would never survive the
|
|
40
|
+
* boundary. Nodes are copied only where a default is applied; everything
|
|
41
|
+
* else is returned by reference.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
function ensureFencedCode(node) {
|
|
46
|
+
let result = node;
|
|
47
|
+
const children = node.children;
|
|
48
|
+
if (children !== void 0) {
|
|
49
|
+
const mapped = children.map(ensureFencedCode);
|
|
50
|
+
if (mapped.some((child, index) => child !== children[index])) result = {
|
|
51
|
+
...node,
|
|
52
|
+
children: mapped
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (result.type === "code" && result.lang === void 0 && result.fenceChar === void 0) return {
|
|
56
|
+
...result,
|
|
57
|
+
fenceChar: "`"
|
|
58
|
+
};
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Serialize a plain MDAST tree to canonical markdown.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* The single emit chokepoint for the changesets pipeline: decodes the plain
|
|
66
|
+
* mdast tree into `@effected/markdown`'s node classes via
|
|
67
|
+
* `Mdast.fromMdastResult` and serializes it with `Markdown.stringifyResult`.
|
|
68
|
+
* Synchronous on purpose — every call site is a sync string pipeline — and
|
|
69
|
+
* failures (an undecodable node type, the nesting-depth hardening guard) are
|
|
70
|
+
* defects on self-built trees, so they throw rather than surface typed.
|
|
71
|
+
*
|
|
72
|
+
* The output always ends with exactly one trailing newline, matching the
|
|
73
|
+
* kit's document-level canonical rule.
|
|
74
|
+
*
|
|
75
|
+
* @param tree - The plain mdast root to serialize
|
|
76
|
+
* @returns Canonical markdown text
|
|
77
|
+
*
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
function emitMarkdown(tree) {
|
|
81
|
+
const fenced = ensureFencedCode(unwrap(Mdast.fromMdastResult(tree), "markdown emit: mdast tree failed to decode"));
|
|
82
|
+
return unwrap(Markdown.stringifyResult(fenced), "markdown emit: canonical stringify failed");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { emitMarkdown };
|
|
@@ -1,93 +1,19 @@
|
|
|
1
|
+
import { emitMarkdown } from "./markdown-emit.js";
|
|
1
2
|
import remarkGfm from "remark-gfm";
|
|
2
3
|
import remarkParse from "remark-parse";
|
|
3
|
-
import remarkStringify from "remark-stringify";
|
|
4
4
|
import { unified } from "unified";
|
|
5
5
|
|
|
6
6
|
//#region src/changesets/utils/remark-pipeline.ts
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* markdown verbatim rather than escaped.
|
|
10
|
-
*
|
|
11
|
-
* @remarks
|
|
12
|
-
* `remark-stringify` escapes any character that could open a markdown
|
|
13
|
-
* construct. Inside a dependency table that is always wrong: with
|
|
14
|
-
* `remark-gfm` enabled `~` is the strikethrough delimiter, so the specifier
|
|
15
|
-
* `~0.2.1` is written as `\~0.2.1`, and `_` in a package name becomes `\_`.
|
|
16
|
-
* The corruption compounds on every re-serialization, and dependency-table
|
|
17
|
-
* cells round-trip through consolidation and PR-body reconstruction.
|
|
18
|
-
*
|
|
19
|
-
* The marker is deliberately narrow. It is set only by the dependency-table
|
|
20
|
-
* cell builder, whose cell vocabulary is closed — package name, dependency
|
|
21
|
-
* type, action, version specifier, and the em-dash sentinel — and none of
|
|
22
|
-
* those can legitimately carry markdown. Prose elsewhere in a changeset keeps
|
|
23
|
-
* the default escaping.
|
|
24
|
-
*
|
|
25
|
-
* @internal
|
|
26
|
-
*/
|
|
27
|
-
const LITERAL_TEXT_MARKER = "silkLiteralText";
|
|
28
|
-
/**
|
|
29
|
-
* Mark a text node so {@link createRemarkProcessor} writes it verbatim.
|
|
30
|
-
*
|
|
31
|
-
* @param value - The literal cell text
|
|
32
|
-
* @returns An MDAST `Text` node carrying the literal marker
|
|
33
|
-
*
|
|
34
|
-
* @internal
|
|
35
|
-
*/
|
|
36
|
-
function literalText(value) {
|
|
37
|
-
return {
|
|
38
|
-
type: "text",
|
|
39
|
-
value,
|
|
40
|
-
data: { [LITERAL_TEXT_MARKER]: true }
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Whether a text node was marked by {@link literalText}.
|
|
45
|
-
*
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
|
-
function isLiteralText(node) {
|
|
49
|
-
return node.data?.[LITERAL_TEXT_MARKER] === true;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Escape the two characters that would otherwise corrupt the table grid.
|
|
53
|
-
*
|
|
54
|
-
* @remarks
|
|
55
|
-
* A `|` would close the cell early and a `\` would be read as an escape
|
|
56
|
-
* introducer, so both are escaped to keep the written cell parseable back to
|
|
57
|
-
* its exact input. Every other character is emitted as-is. Because parsing
|
|
58
|
-
* consumes the backslash, a value that already carries a legacy `\~` from the
|
|
59
|
-
* old escaping path re-parses to its clean form rather than accumulating
|
|
60
|
-
* another layer.
|
|
61
|
-
*
|
|
62
|
-
* @internal
|
|
63
|
-
*/
|
|
64
|
-
function escapeCellLiteral(value) {
|
|
65
|
-
return value.replace(/[\\|]/g, "\\$&");
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* `mdast-util-to-markdown` handler overrides used by the shared processor.
|
|
69
|
-
*
|
|
70
|
-
* @remarks
|
|
71
|
-
* Replaces the default `text` handler — which is `state.safe(node.value, info)`
|
|
72
|
-
* — with one that bypasses escaping for nodes marked by {@link literalText}
|
|
73
|
-
* and delegates to the default behavior for everything else.
|
|
74
|
-
*
|
|
75
|
-
* @internal
|
|
76
|
-
*/
|
|
77
|
-
const literalTextHandlers = { text: (node, _parent, state, info) => {
|
|
78
|
-
const text = node;
|
|
79
|
-
return isLiteralText(text) ? escapeCellLiteral(text.value) : state.safe(text.value, info);
|
|
80
|
-
} };
|
|
81
|
-
/**
|
|
82
|
-
* Create a unified processor configured with remark-parse, remark-gfm,
|
|
83
|
-
* and remark-stringify.
|
|
8
|
+
* Create a unified processor configured with remark-parse and remark-gfm.
|
|
84
9
|
*
|
|
85
10
|
* @remarks
|
|
86
11
|
* Each call creates a fresh processor instance. The plugin chain is:
|
|
87
12
|
* 1. `remark-parse` — markdown to MDAST
|
|
88
13
|
* 2. `remark-gfm` — GitHub Flavored Markdown extensions (tables, etc.)
|
|
89
|
-
*
|
|
90
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* The processor carries no compiler: stringification goes through
|
|
16
|
+
* {@link stringifyMarkdown}, never `processor.stringify`.
|
|
91
17
|
*
|
|
92
18
|
* @privateRemarks
|
|
93
19
|
* Return type is intentionally inferred because the unified `Processor`
|
|
@@ -98,7 +24,7 @@ const literalTextHandlers = { text: (node, _parent, state, info) => {
|
|
|
98
24
|
* @internal
|
|
99
25
|
*/
|
|
100
26
|
function createRemarkProcessor() {
|
|
101
|
-
return unified().use(remarkParse).use(remarkGfm)
|
|
27
|
+
return unified().use(remarkParse).use(remarkGfm);
|
|
102
28
|
}
|
|
103
29
|
/**
|
|
104
30
|
* Parse a markdown string into an MDAST AST synchronously.
|
|
@@ -126,11 +52,14 @@ function parseMarkdown(content) {
|
|
|
126
52
|
return createRemarkProcessor().parse(content);
|
|
127
53
|
}
|
|
128
54
|
/**
|
|
129
|
-
* Stringify an MDAST AST back to
|
|
55
|
+
* Stringify an MDAST AST back to markdown text synchronously.
|
|
130
56
|
*
|
|
131
57
|
* @remarks
|
|
132
|
-
*
|
|
133
|
-
* (
|
|
58
|
+
* Delegates to the canonical `@effected/markdown` emit boundary
|
|
59
|
+
* (`emitMarkdown`), so the output is the kit's canonical form — ATX
|
|
60
|
+
* headings, `-` bullets, `*`/`**` emphasis, `***` thematic breaks, one
|
|
61
|
+
* blank line between blocks, a single trailing newline. GFM constructs
|
|
62
|
+
* (tables, strikethrough) are serialized natively.
|
|
134
63
|
*
|
|
135
64
|
* @param tree - The MDAST root node
|
|
136
65
|
* @returns The serialized markdown string
|
|
@@ -147,8 +76,8 @@ function parseMarkdown(content) {
|
|
|
147
76
|
* @internal
|
|
148
77
|
*/
|
|
149
78
|
function stringifyMarkdown(tree) {
|
|
150
|
-
return
|
|
79
|
+
return emitMarkdown(tree);
|
|
151
80
|
}
|
|
152
81
|
|
|
153
82
|
//#endregion
|
|
154
|
-
export {
|
|
83
|
+
export { createRemarkProcessor, parseMarkdown, stringifyMarkdown };
|
|
@@ -60,12 +60,14 @@ function parseChangesetSections(summary) {
|
|
|
60
60
|
const headingNode = tree.children[headingIndex];
|
|
61
61
|
const headingText = toString(headingNode);
|
|
62
62
|
const nextIndex = i + 1 < h2Indices.length ? h2Indices[i + 1] : tree.children.length;
|
|
63
|
-
const
|
|
63
|
+
const contentNodes = tree.children.slice(headingIndex + 1, nextIndex);
|
|
64
|
+
const content = stringifyAstSlice(contentNodes);
|
|
64
65
|
const category = fromHeading(headingText);
|
|
65
66
|
if (category) result.sections.push({
|
|
66
67
|
category,
|
|
67
68
|
heading: headingText,
|
|
68
|
-
content
|
|
69
|
+
content,
|
|
70
|
+
contentNodes
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
return result;
|