@savvy-web/silk-effects 9.5.4 → 9.5.5
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,56 +1,23 @@
|
|
|
1
1
|
import { resolveCommitType } from "../categories/index.js";
|
|
2
2
|
import { GitHubService } from "../services/github.js";
|
|
3
3
|
import { logWarning } from "../utils/logger.js";
|
|
4
|
-
import {
|
|
4
|
+
import { emitKitMarkdown, parseKitMarkdown } from "../utils/markdown-emit.js";
|
|
5
5
|
import { GitHubInfoSchema } from "../schemas/github.js";
|
|
6
6
|
import { parseCommitMessage } from "../utils/commit-parser.js";
|
|
7
7
|
import { parseIssueReferences } from "../utils/issue-refs.js";
|
|
8
|
-
import {
|
|
8
|
+
import { sectionsOf } from "../utils/section-parser.js";
|
|
9
9
|
import { formatChangelogEntry, formatPRAndUserAttribution } from "./formatting.js";
|
|
10
|
-
import { Effect, Schema } from "effect";
|
|
10
|
+
import { Effect, Result, Schema } from "effect";
|
|
11
|
+
import { Markdown, MarkdownTransform, Paragraph, Text } from "@effected/markdown";
|
|
11
12
|
|
|
12
13
|
//#region src/changesets/changelog/getReleaseLine.ts
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Formats a single changeset into a changelog entry. This is the primary
|
|
17
|
-
* formatting function in the changelog pipeline, responsible for transforming
|
|
18
|
-
* a `NewChangesetWithCommit` into structured markdown output.
|
|
19
|
-
*
|
|
20
|
-
* @remarks
|
|
21
|
-
* The formatter supports two distinct input modes:
|
|
22
|
-
*
|
|
23
|
-
* 1. **Section-aware changesets** — When the changeset summary contains h2
|
|
24
|
-
* headings (e.g., `## Features`, `## Bug Fixes`), each section is rendered
|
|
25
|
-
* as an h3 heading with list items beneath it. This mode
|
|
26
|
-
* produces multi-line output suitable for rich changelogs.
|
|
27
|
-
*
|
|
28
|
-
* 2. **Flat-text changesets** — When the summary is plain text without section
|
|
29
|
-
* headings, the formatter falls back to backward-compatible single-line
|
|
30
|
-
* output. The conventional commit type is resolved via `resolveCommitType`
|
|
31
|
-
* to determine the changelog category.
|
|
32
|
-
*
|
|
33
|
-
* In both modes, the formatter:
|
|
34
|
-
* - Fetches GitHub metadata (PR number, author) via {@link GitHubService}
|
|
35
|
-
* - Extracts and renders issue references (Closes, Fixes, Refs)
|
|
36
|
-
* - Appends PR and user attribution when available
|
|
37
|
-
*
|
|
38
|
-
* @see {@link formatChangelogEntry} for the low-level entry formatting
|
|
39
|
-
* @see {@link formatPRAndUserAttribution} for attribution suffix generation
|
|
40
|
-
* @see {@link getDependencyReleaseLine} for the companion dependency formatter
|
|
41
|
-
*
|
|
42
|
-
* @internal
|
|
43
|
-
*/
|
|
44
|
-
/**
|
|
45
|
-
* Stringify a single MDAST block node back to markdown, trimmed.
|
|
15
|
+
* Stringify a single block node back to markdown, trimmed.
|
|
46
16
|
*
|
|
47
17
|
* @internal
|
|
48
18
|
*/
|
|
49
19
|
function stringifyNode(node) {
|
|
50
|
-
return
|
|
51
|
-
type: "root",
|
|
52
|
-
children: [node]
|
|
53
|
-
}).trimEnd();
|
|
20
|
+
return emitKitMarkdown(node).trimEnd();
|
|
54
21
|
}
|
|
55
22
|
/**
|
|
56
23
|
* Render one top-level section content node to markdown.
|
|
@@ -77,13 +44,7 @@ function renderSectionNode(node) {
|
|
|
77
44
|
const [first, ...rest] = stringifyNode(node).split("\n");
|
|
78
45
|
return [`- ${first}`, ...rest.map((line) => line.length > 0 ? ` ${line}` : line)].join("\n");
|
|
79
46
|
}
|
|
80
|
-
if (node.type === "heading") {
|
|
81
|
-
const heading = node;
|
|
82
|
-
return stringifyNode({
|
|
83
|
-
...heading,
|
|
84
|
-
depth: Math.min(Math.max(heading.depth + 1, 4), 6)
|
|
85
|
-
});
|
|
86
|
-
}
|
|
47
|
+
if (node.type === "heading") return stringifyNode(MarkdownTransform.shiftHeadings(node, 1, { min: 4 }));
|
|
87
48
|
return stringifyNode(node);
|
|
88
49
|
}
|
|
89
50
|
/**
|
|
@@ -96,12 +57,9 @@ function renderSectionNode(node) {
|
|
|
96
57
|
function attributionPhrasing(attribution) {
|
|
97
58
|
const trimmed = attribution.trim();
|
|
98
59
|
if (trimmed === "") return void 0;
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
return [{
|
|
102
|
-
type: "text",
|
|
103
|
-
value: " "
|
|
104
|
-
}, ...first.children];
|
|
60
|
+
const parsed = Markdown.parsePhrasingResult(trimmed);
|
|
61
|
+
if (Result.isFailure(parsed) || parsed.success.length === 0) return void 0;
|
|
62
|
+
return [new Text({ value: " " }), ...parsed.success];
|
|
105
63
|
}
|
|
106
64
|
/**
|
|
107
65
|
* Find the paragraph of a list's FINAL RENDERED bullet: the last paragraph
|
|
@@ -178,7 +136,8 @@ function getReleaseLine(changeset, versionType, options) {
|
|
|
178
136
|
commit: changeset.commit,
|
|
179
137
|
repo: options.repo
|
|
180
138
|
}).pipe(Effect.flatMap((raw) => Schema.decodeUnknownEffect(GitHubInfoSchema)(raw).pipe(Effect.map(() => raw), Effect.catch(() => Effect.succeed(raw)))), Effect.catch((error) => logWarning("Could not fetch GitHub info for commit:", changeset.commit ?? "", String(error)).pipe(Effect.as(null))));
|
|
181
|
-
const
|
|
139
|
+
const tree = parseKitMarkdown(changeset.summary);
|
|
140
|
+
let parsed = sectionsOf(tree, changeset.summary);
|
|
182
141
|
const firstLine = changeset.summary.split("\n")[0];
|
|
183
142
|
const commitMsg = parseCommitMessage(firstLine);
|
|
184
143
|
const bodyText = changeset.summary.split("\n").slice(1).join("\n");
|
|
@@ -190,8 +149,13 @@ function getReleaseLine(changeset, versionType, options) {
|
|
|
190
149
|
let standaloneAttribution;
|
|
191
150
|
if (phrasing) {
|
|
192
151
|
const target = findAttributionTarget(parsed.sections);
|
|
193
|
-
if (target)
|
|
194
|
-
|
|
152
|
+
if (target) {
|
|
153
|
+
const attributed = MarkdownTransform.map(tree, { paragraph: (para) => para === target ? new Paragraph({
|
|
154
|
+
...para,
|
|
155
|
+
children: [...para.children, ...phrasing]
|
|
156
|
+
}) : para });
|
|
157
|
+
parsed = sectionsOf(attributed, changeset.summary);
|
|
158
|
+
} else standaloneAttribution = attribution.trim();
|
|
195
159
|
}
|
|
196
160
|
const blocks = [];
|
|
197
161
|
if (parsed.preamble) blocks.push(parsed.preamble);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Result } from "effect";
|
|
2
|
-
import { Markdown, Mdast } from "@effected/markdown";
|
|
2
|
+
import { Code, Markdown, MarkdownTransform, Mdast } from "@effected/markdown";
|
|
3
3
|
|
|
4
4
|
//#region src/changesets/utils/markdown-emit.ts
|
|
5
5
|
/**
|
|
@@ -37,26 +37,16 @@ function unwrap(result, context) {
|
|
|
37
37
|
* the kit's own per-node fidelity field, `fenceChar`, post-decode. It has to
|
|
38
38
|
* happen post-decode: `Mdast.fromMdast` drops non-mdast fields from foreign
|
|
39
39
|
* trees, so a `fenceChar` set on the plain tree would never survive the
|
|
40
|
-
* boundary.
|
|
41
|
-
*
|
|
40
|
+
* boundary. `MarkdownTransform.map` rebuilds only the paths to a changed
|
|
41
|
+
* code node; every other subtree is returned by reference.
|
|
42
42
|
*
|
|
43
43
|
* @internal
|
|
44
44
|
*/
|
|
45
45
|
function ensureFencedCode(node) {
|
|
46
|
-
|
|
47
|
-
|
|
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,
|
|
46
|
+
return MarkdownTransform.map(node, { code: (code) => code.lang === void 0 && code.fenceChar === void 0 ? new Code({
|
|
47
|
+
...code,
|
|
57
48
|
fenceChar: "`"
|
|
58
|
-
};
|
|
59
|
-
return result;
|
|
49
|
+
}) : code });
|
|
60
50
|
}
|
|
61
51
|
/**
|
|
62
52
|
* Serialize a plain MDAST tree to canonical markdown.
|
|
@@ -81,6 +71,40 @@ function emitMarkdown(tree) {
|
|
|
81
71
|
const fenced = ensureFencedCode(unwrap(Mdast.fromMdastResult(tree), "markdown emit: mdast tree failed to decode"));
|
|
82
72
|
return unwrap(Markdown.stringifyResult(fenced), "markdown emit: canonical stringify failed");
|
|
83
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse markdown text straight into `@effected/markdown`'s node classes (GFM
|
|
76
|
+
* dialect, frontmatter capture off).
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* Synchronous for the same reason as {@link emitMarkdown}; the kit's parser
|
|
80
|
+
* is total over text, so a failure here is a defect (the nesting-depth
|
|
81
|
+
* hardening guard), not an operational condition.
|
|
82
|
+
*
|
|
83
|
+
* @param text - The markdown source
|
|
84
|
+
* @returns The parsed kit root
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
function parseKitMarkdown(text) {
|
|
89
|
+
const parsed = unwrap(Markdown.parseResult(text), "markdown parse: canonical parse failed");
|
|
90
|
+
return unwrap(Mdast.fromMdastResult(Mdast.toMdast(parsed)), "markdown parse: fidelity strip failed");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Serialize a kit root, or a single kit block node, to canonical markdown.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* The kit-tree twin of {@link emitMarkdown}: the same fence normalization,
|
|
97
|
+
* then `Markdown.stringifyResult`. A lone block node emits byte-identically
|
|
98
|
+
* to a one-child root, trailing newline included.
|
|
99
|
+
*
|
|
100
|
+
* @param node - The kit root or block node to serialize
|
|
101
|
+
* @returns Canonical markdown text
|
|
102
|
+
*
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
function emitKitMarkdown(node) {
|
|
106
|
+
return unwrap(Markdown.stringifyResult(ensureFencedCode(node)), "markdown emit: canonical stringify failed");
|
|
107
|
+
}
|
|
84
108
|
|
|
85
109
|
//#endregion
|
|
86
|
-
export { emitMarkdown };
|
|
110
|
+
export { emitKitMarkdown, emitMarkdown, parseKitMarkdown };
|
|
@@ -5,53 +5,6 @@ import { unified } from "unified";
|
|
|
5
5
|
|
|
6
6
|
//#region src/changesets/utils/remark-pipeline.ts
|
|
7
7
|
/**
|
|
8
|
-
* Create a unified processor configured with remark-parse and remark-gfm.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Each call creates a fresh processor instance. The plugin chain is:
|
|
12
|
-
* 1. `remark-parse` — markdown to MDAST
|
|
13
|
-
* 2. `remark-gfm` — GitHub Flavored Markdown extensions (tables, etc.)
|
|
14
|
-
*
|
|
15
|
-
* The processor carries no compiler: stringification goes through
|
|
16
|
-
* {@link stringifyMarkdown}, never `processor.stringify`.
|
|
17
|
-
*
|
|
18
|
-
* @privateRemarks
|
|
19
|
-
* Return type is intentionally inferred because the unified `Processor`
|
|
20
|
-
* generic signature is complex and parameterized by the plugin chain.
|
|
21
|
-
*
|
|
22
|
-
* @returns A configured unified processor
|
|
23
|
-
*
|
|
24
|
-
* @internal
|
|
25
|
-
*/
|
|
26
|
-
function createRemarkProcessor() {
|
|
27
|
-
return unified().use(remarkParse).use(remarkGfm);
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Parse a markdown string into an MDAST AST synchronously.
|
|
31
|
-
*
|
|
32
|
-
* @remarks
|
|
33
|
-
* Uses {@link createRemarkProcessor} internally. The returned tree
|
|
34
|
-
* includes GFM extensions (tables, strikethrough, etc.) thanks to
|
|
35
|
-
* the remark-gfm plugin.
|
|
36
|
-
*
|
|
37
|
-
* @param content - Raw markdown string
|
|
38
|
-
* @returns The parsed MDAST root node
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```typescript
|
|
42
|
-
* import { parseMarkdown } from "../utils/remark-pipeline.js";
|
|
43
|
-
*
|
|
44
|
-
* const tree = parseMarkdown("## Version 1.0.0\n\nInitial release.");
|
|
45
|
-
* // tree.type === "root"
|
|
46
|
-
* // tree.children[0].type === "heading"
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* @internal
|
|
50
|
-
*/
|
|
51
|
-
function parseMarkdown(content) {
|
|
52
|
-
return createRemarkProcessor().parse(content);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
8
|
* Stringify an MDAST AST back to markdown text synchronously.
|
|
56
9
|
*
|
|
57
10
|
* @remarks
|
|
@@ -80,4 +33,4 @@ function stringifyMarkdown(tree) {
|
|
|
80
33
|
}
|
|
81
34
|
|
|
82
35
|
//#endregion
|
|
83
|
-
export {
|
|
36
|
+
export { stringifyMarkdown };
|
|
@@ -1,96 +1,66 @@
|
|
|
1
1
|
import { fromHeading } from "../categories/index.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { emitKitMarkdown } from "./markdown-emit.js";
|
|
3
|
+
import { MarkdownTransform, Mdast, Root } from "@effected/markdown";
|
|
4
4
|
|
|
5
5
|
//#region src/changesets/utils/section-parser.ts
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Slice an already-parsed changeset tree into categorized sections.
|
|
8
8
|
*
|
|
9
9
|
* @remarks
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* {@link SectionCategory} via `fromHeading()`, and collect all nodes
|
|
16
|
-
* between this h2 and the next h2 (or end of document) as the section
|
|
17
|
-
* content
|
|
10
|
+
* Split from {@link parseChangesetSections} so a caller can transform the
|
|
11
|
+
* tree (the renderer appends attribution with `MarkdownTransform.map`) and
|
|
12
|
+
* re-slice it without re-parsing. A section runs from its h2 to the next
|
|
13
|
+
* root-level h2 (or the end of the summary); a stray h1 inside a section stays
|
|
14
|
+
* in that section's content, as it always has.
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* If no h2 headings are present, the entire content is returned as the preamble
|
|
24
|
-
* with an empty sections array (backward-compatible flat-text mode).
|
|
25
|
-
*
|
|
26
|
-
* Unknown headings (those not recognized by `fromHeading()`) are silently
|
|
27
|
-
* skipped; validation of heading names is the responsibility of Layer 1
|
|
28
|
-
* (remark-lint).
|
|
29
|
-
*
|
|
30
|
-
* @param summary - The changeset summary markdown
|
|
16
|
+
* @param tree - The parsed summary
|
|
17
|
+
* @param summary - The summary source, returned verbatim as the preamble in
|
|
18
|
+
* flat-text mode
|
|
31
19
|
* @returns Parsed sections and optional preamble
|
|
32
20
|
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* import { parseChangesetSections } from "../utils/section-parser.js";
|
|
36
|
-
*
|
|
37
|
-
* const result = parseChangesetSections("## Features\n\n- New API\n\n## Bug Fixes\n\n- Fixed crash");
|
|
38
|
-
* // result.sections.length === 2
|
|
39
|
-
* // result.sections[0].category === "Features"
|
|
40
|
-
* // result.preamble === undefined
|
|
41
|
-
* ```
|
|
42
|
-
*
|
|
43
21
|
* @internal
|
|
44
22
|
*/
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
for (let i = 0; i < tree.children.length; i++) {
|
|
49
|
-
const node = tree.children[i];
|
|
50
|
-
if (node.type === "heading" && node.depth === 2) h2Indices.push(i);
|
|
51
|
-
}
|
|
52
|
-
if (h2Indices.length === 0) return {
|
|
23
|
+
function sectionsOf(tree, summary) {
|
|
24
|
+
const spans = MarkdownTransform.sections(tree, { depth: 2 });
|
|
25
|
+
if (spans.length === 0) return {
|
|
53
26
|
preamble: summary.trim(),
|
|
54
27
|
sections: []
|
|
55
28
|
};
|
|
56
29
|
const result = { sections: [] };
|
|
57
|
-
if (
|
|
58
|
-
for (let i = 0; i <
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const contentNodes = tree.children.slice(headingIndex + 1, nextIndex);
|
|
64
|
-
const content = stringifyAstSlice(contentNodes);
|
|
30
|
+
if (spans[0].headingIndex > 0) result.preamble = stringifyAstSlice(tree.children.slice(0, spans[0].headingIndex));
|
|
31
|
+
for (let i = 0; i < spans.length; i++) {
|
|
32
|
+
const span = spans[i];
|
|
33
|
+
const headingText = Mdast.toText(span.heading);
|
|
34
|
+
const end = i + 1 < spans.length ? spans[i + 1].headingIndex : tree.children.length;
|
|
35
|
+
const contentNodes = tree.children.slice(span.headingIndex + 1, end).filter(isFlowContent);
|
|
65
36
|
const category = fromHeading(headingText);
|
|
66
37
|
if (category) result.sections.push({
|
|
67
38
|
category,
|
|
68
39
|
heading: headingText,
|
|
69
|
-
content,
|
|
40
|
+
content: stringifyAstSlice(contentNodes),
|
|
70
41
|
contentNodes
|
|
71
42
|
});
|
|
72
43
|
}
|
|
73
44
|
return result;
|
|
74
45
|
}
|
|
75
46
|
/**
|
|
76
|
-
*
|
|
47
|
+
* Narrow a root child to block content (frontmatter capture is off and the
|
|
48
|
+
* dialect is GFM, so this only ever drops nothing).
|
|
77
49
|
*
|
|
78
|
-
* @
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
function isFlowContent(node) {
|
|
53
|
+
return node.type !== "frontmatter" && node.type !== "mdxjsEsm";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Stringify a slice of root children back to trimmed markdown.
|
|
84
57
|
*
|
|
85
58
|
* @internal
|
|
86
59
|
*/
|
|
87
60
|
function stringifyAstSlice(nodes) {
|
|
88
61
|
if (nodes.length === 0) return "";
|
|
89
|
-
return
|
|
90
|
-
type: "root",
|
|
91
|
-
children: nodes
|
|
92
|
-
}).trim();
|
|
62
|
+
return emitKitMarkdown(new Root({ children: nodes })).trim();
|
|
93
63
|
}
|
|
94
64
|
|
|
95
65
|
//#endregion
|
|
96
|
-
export {
|
|
66
|
+
export { sectionsOf };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/silk-effects",
|
|
3
|
-
"version": "9.5.
|
|
3
|
+
"version": "9.5.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Effect library for Silk Suite conventions",
|
|
6
6
|
"homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
|
|
@@ -40,14 +40,13 @@
|
|
|
40
40
|
"@effected/github-references": "^0.10.0",
|
|
41
41
|
"@effected/glob": "^0.13.0",
|
|
42
42
|
"@effected/jsonc": "^0.18.0",
|
|
43
|
-
"@effected/markdown": "^0.
|
|
43
|
+
"@effected/markdown": "^0.19.0",
|
|
44
44
|
"@effected/package-json": "^0.23.0",
|
|
45
45
|
"@effected/toml": "^0.14.0",
|
|
46
46
|
"@effected/walker": "^0.18.0",
|
|
47
47
|
"@effected/yaml": "^0.22.0",
|
|
48
48
|
"@manypkg/get-packages": "^3.1.0",
|
|
49
49
|
"@savvy-web/silk-core": "0.4.14",
|
|
50
|
-
"mdast-util-heading-range": "^4.0.0",
|
|
51
50
|
"mdast-util-to-string": "^4.0.0",
|
|
52
51
|
"remark-gfm": "^4.0.1",
|
|
53
52
|
"remark-parse": "^11.0.0",
|