@savvy-web/silk-effects 9.5.3 → 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 { parseMarkdown, stringifyMarkdown } from "../utils/remark-pipeline.js";
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 { parseChangesetSections } from "../utils/section-parser.js";
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
- * Core formatter: getReleaseLine.
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 stringifyMarkdown({
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 first = parseMarkdown(trimmed).children[0];
100
- if (first?.type !== "paragraph") return void 0;
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 parsed = parseChangesetSections(changeset.summary);
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) target.children.push(...phrasing);
194
- else standaloneAttribution = attribution.trim();
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. Nodes are copied only where a default is applied; everything
41
- * else is returned by reference.
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
- 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,
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 { createRemarkProcessor, parseMarkdown, stringifyMarkdown };
36
+ export { stringifyMarkdown };
@@ -1,96 +1,66 @@
1
1
  import { fromHeading } from "../categories/index.js";
2
- import { parseMarkdown, stringifyMarkdown } from "./remark-pipeline.js";
3
- import { toString } from "mdast-util-to-string";
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
- * Parse a changeset summary into structured sections.
7
+ * Slice an already-parsed changeset tree into categorized sections.
8
8
  *
9
9
  * @remarks
10
- * The algorithm works in three steps:
11
- * 1. Parse the summary markdown into an MDAST tree via {@link parseMarkdown}
12
- * 2. Scan `tree.children` for h2 (`depth === 2`) headings, recording their
13
- * indices
14
- * 3. For each h2, extract the heading text, resolve it to a
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
- * If the summary contains h2 (`##`) headings, they are mapped to categories
20
- * via `fromHeading()`. Content between headings becomes the section content.
21
- * Content before the first h2 becomes the preamble.
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 parseChangesetSections(summary) {
46
- const tree = parseMarkdown(summary);
47
- const h2Indices = [];
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 (h2Indices[0] > 0) result.preamble = stringifyAstSlice(tree.children.slice(0, h2Indices[0]));
58
- for (let i = 0; i < h2Indices.length; i++) {
59
- const headingIndex = h2Indices[i];
60
- const headingNode = tree.children[headingIndex];
61
- const headingText = toString(headingNode);
62
- const nextIndex = i + 1 < h2Indices.length ? h2Indices[i + 1] : tree.children.length;
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
- * Stringify a slice of MDAST nodes back to markdown.
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
- * @remarks
79
- * Wraps the nodes in a synthetic root node and delegates to
80
- * {@link stringifyMarkdown}. Returns an empty string for empty slices.
81
- *
82
- * @param nodes - Array of MDAST content nodes
83
- * @returns Trimmed markdown string, or empty string if no nodes
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 stringifyMarkdown({
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 { parseChangesetSections };
66
+ export { sectionsOf };
package/index.d.ts CHANGED
@@ -7500,7 +7500,7 @@ interface PnpmWorkspaceContent {
7500
7500
  * This handler processes the workspace file entirely in JavaScript:
7501
7501
  * - Sorts the `packages` array alphabetically (if present)
7502
7502
  * - Sorts `onlyBuiltDependencies` and `publicHoistPattern` arrays (if present)
7503
- * - Sorts all top-level keys alphabetically, keeping `packages` first
7503
+ * - Sorts all top-level keys alphabetically, `packages` included
7504
7504
  * - Formats the output with consistent YAML styling
7505
7505
  * - Validates the YAML structure
7506
7506
  *
@@ -7541,7 +7541,7 @@ declare class PnpmWorkspace {
7541
7541
  * - `packages` array alphabetically
7542
7542
  * - `onlyBuiltDependencies` array (if present)
7543
7543
  * - `publicHoistPattern` array (if present)
7544
- * - All keys alphabetically, keeping `packages` first
7544
+ * - All keys alphabetically, `packages` included
7545
7545
  *
7546
7546
  * @param content - Parsed pnpm-workspace.yaml content
7547
7547
  * @returns Sorted content
@@ -38,7 +38,7 @@ const DEFAULT_STRINGIFY_OPTIONS = new YamlStringifyOptions({
38
38
  * This handler processes the workspace file entirely in JavaScript:
39
39
  * - Sorts the `packages` array alphabetically (if present)
40
40
  * - Sorts `onlyBuiltDependencies` and `publicHoistPattern` arrays (if present)
41
- * - Sorts all top-level keys alphabetically, keeping `packages` first
41
+ * - Sorts all top-level keys alphabetically, `packages` included
42
42
  * - Formats the output with consistent YAML styling
43
43
  * - Validates the YAML structure
44
44
  *
@@ -83,18 +83,14 @@ var PnpmWorkspace = class PnpmWorkspace {
83
83
  * - `packages` array alphabetically
84
84
  * - `onlyBuiltDependencies` array (if present)
85
85
  * - `publicHoistPattern` array (if present)
86
- * - All keys alphabetically, keeping `packages` first
86
+ * - All keys alphabetically, `packages` included
87
87
  *
88
88
  * @param content - Parsed pnpm-workspace.yaml content
89
89
  * @returns Sorted content
90
90
  */
91
91
  static sortContent(content) {
92
92
  const result = {};
93
- const keys = Object.keys(content).sort((a, b) => {
94
- if (a === "packages") return -1;
95
- if (b === "packages") return 1;
96
- return a.localeCompare(b);
97
- });
93
+ const keys = Object.keys(content).sort((a, b) => a.localeCompare(b));
98
94
  for (const key of keys) {
99
95
  const value = content[key];
100
96
  if (PnpmWorkspace.SORTABLE_ARRAY_KEYS.has(key) && Array.isArray(value)) result[key] = [...value].sort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "9.5.3",
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",
@@ -37,17 +37,16 @@
37
37
  "@changesets/get-release-plan": "^5.0.2",
38
38
  "@changesets/pre": "^3.0.0",
39
39
  "@changesets/read": "^1.0.1",
40
- "@effected/github-references": "^0.9.0",
41
- "@effected/glob": "^0.12.0",
42
- "@effected/jsonc": "^0.17.0",
43
- "@effected/markdown": "^0.17.0",
44
- "@effected/package-json": "^0.22.0",
45
- "@effected/toml": "^0.13.0",
46
- "@effected/walker": "^0.17.0",
47
- "@effected/yaml": "^0.21.0",
40
+ "@effected/github-references": "^0.10.0",
41
+ "@effected/glob": "^0.13.0",
42
+ "@effected/jsonc": "^0.18.0",
43
+ "@effected/markdown": "^0.19.0",
44
+ "@effected/package-json": "^0.23.0",
45
+ "@effected/toml": "^0.14.0",
46
+ "@effected/walker": "^0.18.0",
47
+ "@effected/yaml": "^0.22.0",
48
48
  "@manypkg/get-packages": "^3.1.0",
49
- "@savvy-web/silk-core": "0.4.13",
50
- "mdast-util-heading-range": "^4.0.0",
49
+ "@savvy-web/silk-core": "0.4.14",
51
50
  "mdast-util-to-string": "^4.0.0",
52
51
  "remark-gfm": "^4.0.1",
53
52
  "remark-parse": "^11.0.0",
@@ -58,10 +57,10 @@
58
57
  "unist-util-visit": "^5.1.0"
59
58
  },
60
59
  "peerDependencies": {
61
- "@effected/commands": "^0.13.0",
62
- "@effected/git": "^0.22.0",
63
- "@effected/templates": "^0.12.0",
64
- "@effected/workspaces": "^0.34.0",
60
+ "@effected/commands": "^0.14.0",
61
+ "@effected/git": "^0.23.0",
62
+ "@effected/templates": "^0.13.0",
63
+ "@effected/workspaces": "^0.35.0",
65
64
  "effect": "^4.0.0"
66
65
  }
67
66
  }