@savvy-web/silk-effects 0.6.1 → 1.0.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 +48 -17
- package/_virtual/_rolldown/runtime.js +18 -0
- package/changesets/api/categories.js +247 -0
- package/changesets/api/changelog.js +134 -0
- package/changesets/api/dependency-table.js +163 -0
- package/changesets/api/linter.js +168 -0
- package/changesets/api/transformer.js +140 -0
- package/changesets/categories/index.js +299 -0
- package/changesets/categories/types.js +66 -0
- package/changesets/changelog/formatting.js +119 -0
- package/changesets/changelog/getDependencyReleaseLine.js +114 -0
- package/changesets/changelog/getReleaseLine.js +122 -0
- package/changesets/changelog/index.js +99 -0
- package/changesets/constants.js +43 -0
- package/changesets/errors.js +305 -0
- package/changesets/index.js +146 -0
- package/changesets/markdownlint/index.js +29 -0
- package/changesets/markdownlint/rules/content-structure.js +98 -0
- package/changesets/markdownlint/rules/dependency-table-format.js +170 -0
- package/changesets/markdownlint/rules/heading-hierarchy.js +61 -0
- package/changesets/markdownlint/rules/required-sections.js +54 -0
- package/changesets/markdownlint/rules/uncategorized-content.js +54 -0
- package/changesets/markdownlint/rules/utils.js +30 -0
- package/changesets/remark/plugins/aggregate-dependency-tables.js +47 -0
- package/changesets/remark/plugins/contributor-footnotes.js +123 -0
- package/changesets/remark/plugins/deduplicate-items.js +30 -0
- package/changesets/remark/plugins/issue-link-refs.js +58 -0
- package/changesets/remark/plugins/merge-sections.js +43 -0
- package/changesets/remark/plugins/normalize-format.js +47 -0
- package/changesets/remark/plugins/reorder-sections.js +34 -0
- package/changesets/remark/presets.js +119 -0
- package/changesets/remark/rules/content-structure.js +22 -0
- package/changesets/remark/rules/dependency-table-format.js +40 -0
- package/changesets/remark/rules/heading-hierarchy.js +19 -0
- package/changesets/remark/rules/required-sections.js +17 -0
- package/changesets/remark/rules/uncategorized-content.js +31 -0
- package/changesets/schemas/changeset.js +146 -0
- package/changesets/schemas/dependency-table.js +189 -0
- package/changesets/schemas/git.js +69 -0
- package/changesets/schemas/github.js +175 -0
- package/changesets/schemas/options.js +182 -0
- package/changesets/schemas/package-scope.js +128 -0
- package/changesets/schemas/primitives.js +72 -0
- package/changesets/schemas/version-files.js +151 -0
- package/changesets/services/branch-analyzer.js +278 -0
- package/changesets/services/changelog.js +50 -0
- package/changesets/services/config-inspector.js +390 -0
- package/changesets/services/github.js +178 -0
- package/changesets/services/markdown.js +106 -0
- package/changesets/services/workspace-snapshot.js +182 -0
- package/changesets/utils/commit-parser.js +80 -0
- package/changesets/utils/dep-diff.js +77 -0
- package/changesets/utils/dependency-table.js +347 -0
- package/changesets/utils/issue-refs.js +101 -0
- package/changesets/utils/jsonpath.js +175 -0
- package/changesets/utils/logger.js +50 -0
- package/changesets/utils/markdown-link.js +57 -0
- package/changesets/utils/publishability.js +39 -0
- package/changesets/utils/remark-pipeline.js +79 -0
- package/changesets/utils/section-parser.js +94 -0
- package/changesets/utils/strip-frontmatter.js +46 -0
- package/changesets/utils/version-blocks.js +108 -0
- package/changesets/utils/version-files.js +336 -0
- package/changesets/utils/worktree-snapshot.js +142 -0
- package/changesets/vendor/github-info.js +55 -0
- package/commitlint/config/factory.js +69 -0
- package/commitlint/config/plugins.js +227 -0
- package/commitlint/config/rules.js +155 -0
- package/commitlint/config/schema.js +46 -0
- package/commitlint/detection/dco.js +53 -0
- package/commitlint/detection/scopes.js +45 -0
- package/commitlint/formatter/format.js +85 -0
- package/commitlint/formatter/messages.js +79 -0
- package/commitlint/hook/diagnostics/branch.js +36 -0
- package/commitlint/hook/diagnostics/cache.js +37 -0
- package/commitlint/hook/diagnostics/commitlint-config.js +36 -0
- package/commitlint/hook/diagnostics/open-issues.js +56 -0
- package/commitlint/hook/diagnostics/package-manager.js +51 -0
- package/commitlint/hook/diagnostics/signing.js +107 -0
- package/commitlint/hook/envelope.js +46 -0
- package/commitlint/hook/output.js +45 -0
- package/commitlint/hook/parse-bash-command.js +105 -0
- package/commitlint/hook/rules/closes-trailer.js +31 -0
- package/commitlint/hook/rules/forbidden-content.js +32 -0
- package/commitlint/hook/rules/plan-leakage.js +36 -0
- package/commitlint/hook/rules/signing-flag-conflict.js +25 -0
- package/commitlint/hook/rules/soft-wrap.js +37 -0
- package/commitlint/hook/rules/types.js +14 -0
- package/commitlint/hook/rules/verbosity.js +31 -0
- package/commitlint/hook/silence-logger.js +39 -0
- package/commitlint/index.js +146 -0
- package/commitlint/prompt/config.js +91 -0
- package/commitlint/prompt/emojis.js +74 -0
- package/commitlint/prompt/prompter.js +135 -0
- package/commitlint/static.js +73 -0
- package/errors/BiomeSyncError.js +21 -0
- package/errors/ChangesetConfigError.js +20 -0
- package/errors/ConfigNotFoundError.js +21 -0
- package/errors/SectionParseError.js +16 -0
- package/errors/SectionValidationError.js +16 -0
- package/errors/SectionWriteError.js +16 -0
- package/errors/TagFormatError.js +20 -0
- package/errors/ToolNotFoundError.js +11 -0
- package/errors/ToolResolutionError.js +11 -0
- package/errors/ToolVersionMismatchError.js +11 -0
- package/errors/VersioningDetectionError.js +20 -0
- package/errors/WorkspaceAnalysisError.js +21 -0
- package/index.d.ts +9743 -8380
- package/index.js +36 -6657
- package/lint/Handler.js +39 -0
- package/lint/cli/sections.js +65 -0
- package/lint/cli/templates/markdownlint.gen.js +183 -0
- package/lint/config/Preset.js +152 -0
- package/lint/config/createConfig.js +89 -0
- package/lint/handlers/Biome.js +179 -0
- package/lint/handlers/Markdown.js +139 -0
- package/lint/handlers/PackageJson.js +130 -0
- package/lint/handlers/PnpmWorkspace.js +141 -0
- package/lint/handlers/ShellScripts.js +58 -0
- package/lint/handlers/TypeScript.js +134 -0
- package/lint/handlers/Yaml.js +167 -0
- package/lint/index.js +52 -0
- package/lint/utils/Command.js +285 -0
- package/lint/utils/Filter.js +100 -0
- package/lint/utils/Workspace.js +86 -0
- package/package.json +52 -63
- package/schemas/CommentStyle.js +16 -0
- package/schemas/ResolvedTool.js +63 -0
- package/schemas/SavvySections.js +113 -0
- package/schemas/SectionBlock.js +70 -0
- package/schemas/SectionDefinition.js +121 -0
- package/schemas/SectionResults.js +12 -0
- package/schemas/TagStrategySchemas.js +18 -0
- package/schemas/ToolDefinition.js +39 -0
- package/schemas/ToolResults.js +14 -0
- package/schemas/VersioningSchemas.js +95 -0
- package/schemas/WorkspaceAnalysisSchemas.js +190 -0
- package/services/BiomeSchemaSync.js +133 -0
- package/services/ChangesetConfig.js +78 -0
- package/services/ChangesetConfigReader.js +106 -0
- package/services/ConfigDiscovery.js +71 -0
- package/services/ManagedSection.js +288 -0
- package/services/SilkPublishability.js +193 -0
- package/services/SilkWorkspaceAnalyzer.js +213 -0
- package/services/TagStrategy.js +54 -0
- package/services/ToolDiscovery.js +229 -0
- package/services/VersioningStrategy.js +67 -0
- package/tsdoc-metadata.json +11 -11
- package/turbo/digest.js +127 -0
- package/turbo/errors.js +48 -0
- package/turbo/index.js +32 -0
- package/turbo/schemas/DryRun.js +57 -0
- package/turbo/schemas/results.js +61 -0
- package/turbo/services/TurboInspector.js +100 -0
- package/utils/ToolCommand.js +40 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { RULE_DOCS } from "../../constants.js";
|
|
2
|
+
import { getHeadingLevel, getHeadingText } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/markdownlint/rules/dependency-table-format.ts
|
|
5
|
+
const EM_DASH = "—";
|
|
6
|
+
const VALID_TYPES = new Set([
|
|
7
|
+
"dependency",
|
|
8
|
+
"devDependency",
|
|
9
|
+
"peerDependency",
|
|
10
|
+
"optionalDependency",
|
|
11
|
+
"workspace",
|
|
12
|
+
"config"
|
|
13
|
+
]);
|
|
14
|
+
const VALID_ACTIONS = new Set([
|
|
15
|
+
"added",
|
|
16
|
+
"updated",
|
|
17
|
+
"removed"
|
|
18
|
+
]);
|
|
19
|
+
const EXPECTED_HEADERS = [
|
|
20
|
+
"dependency",
|
|
21
|
+
"type",
|
|
22
|
+
"action",
|
|
23
|
+
"from",
|
|
24
|
+
"to"
|
|
25
|
+
];
|
|
26
|
+
const VERSION_RE = /^(\u2014|[~^]?\d+\.\d+\.\d+(?:[-+.][\w.+-]*)?)$/;
|
|
27
|
+
/**
|
|
28
|
+
* Extract text from a `tableHeader` or `tableData` cell token.
|
|
29
|
+
*
|
|
30
|
+
* The cell contains: `tableCellDivider`, whitespace, `tableContent`, whitespace.
|
|
31
|
+
* The `tableContent` token has a `.text` property with the cell value.
|
|
32
|
+
*
|
|
33
|
+
* @param cell - A GFM table cell token (header or data)
|
|
34
|
+
* @returns The trimmed text content of the cell, or an empty string
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
function getCellText(cell) {
|
|
39
|
+
const content = cell.children.find((c) => c.type === "tableContent");
|
|
40
|
+
return content ? content.text.trim() : "";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Extract all cell texts from a `tableRow` token.
|
|
44
|
+
*
|
|
45
|
+
* @param row - A GFM `tableRow` token
|
|
46
|
+
* @returns An array of trimmed cell text values
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
function getRowCells(row) {
|
|
51
|
+
return row.children.filter((c) => c.type === "tableHeader" || c.type === "tableData").map(getCellText);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The markdownlint `Rule` object for CSH005 (`changeset-dependency-table-format`).
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
const DependencyTableFormatRule = {
|
|
59
|
+
names: ["changeset-dependency-table-format", "CSH005"],
|
|
60
|
+
description: "Dependencies section must contain a valid dependency table",
|
|
61
|
+
tags: ["changeset"],
|
|
62
|
+
parser: "micromark",
|
|
63
|
+
function: function CSH005(params, onError) {
|
|
64
|
+
const tokens = params.parsers.micromark.tokens;
|
|
65
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
66
|
+
const token = tokens[i];
|
|
67
|
+
if (token.type !== "atxHeading") continue;
|
|
68
|
+
if (getHeadingLevel(token) !== 2) continue;
|
|
69
|
+
if (getHeadingText(token).toLowerCase() !== "dependencies") continue;
|
|
70
|
+
const headingLine = token.startLine;
|
|
71
|
+
let tableToken = null;
|
|
72
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
73
|
+
const next = tokens[j];
|
|
74
|
+
if (next.type === "lineEnding" || next.type === "lineEndingBlank") continue;
|
|
75
|
+
if (next.type === "atxHeading") break;
|
|
76
|
+
if (next.type === "table") tableToken = next;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (tableToken === null) {
|
|
80
|
+
onError({
|
|
81
|
+
lineNumber: headingLine,
|
|
82
|
+
detail: `Dependencies section must contain a table, not a list or paragraph. See: ${RULE_DOCS.CSH005}`
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const tableHead = tableToken.children.find((c) => c.type === "tableHead");
|
|
87
|
+
if (!tableHead) {
|
|
88
|
+
onError({
|
|
89
|
+
lineNumber: tableToken.startLine,
|
|
90
|
+
detail: `Dependencies table is missing a header row. See: ${RULE_DOCS.CSH005}`
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const headerRow = tableHead.children.find((c) => c.type === "tableRow");
|
|
95
|
+
if (!headerRow) {
|
|
96
|
+
onError({
|
|
97
|
+
lineNumber: tableToken.startLine,
|
|
98
|
+
detail: `Dependencies table is missing a header row. See: ${RULE_DOCS.CSH005}`
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const headers = getRowCells(headerRow).map((h) => h.toLowerCase());
|
|
103
|
+
if (headers.length !== EXPECTED_HEADERS.length || !headers.every((h, idx) => h === EXPECTED_HEADERS[idx])) {
|
|
104
|
+
onError({
|
|
105
|
+
lineNumber: headerRow.startLine,
|
|
106
|
+
detail: `Dependencies table must have columns: Dependency, Type, Action, From, To. Got: ${headers.join(", ")}. See: ${RULE_DOCS.CSH005}`
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const tableBody = tableToken.children.find((c) => c.type === "tableBody");
|
|
111
|
+
if (!tableBody) {
|
|
112
|
+
onError({
|
|
113
|
+
lineNumber: tableToken.startLine,
|
|
114
|
+
detail: `Dependencies table must have at least one data row. See: ${RULE_DOCS.CSH005}`
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const dataRows = tableBody.children.filter((c) => c.type === "tableRow");
|
|
119
|
+
if (dataRows.length === 0) {
|
|
120
|
+
onError({
|
|
121
|
+
lineNumber: tableToken.startLine,
|
|
122
|
+
detail: `Dependencies table must have at least one data row. See: ${RULE_DOCS.CSH005}`
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const row of dataRows) {
|
|
127
|
+
const cells = getRowCells(row);
|
|
128
|
+
if (cells.length < 5) {
|
|
129
|
+
onError({
|
|
130
|
+
lineNumber: row.startLine,
|
|
131
|
+
detail: `Dependencies table row has too few columns (expected 5, got ${cells.length}). See: ${RULE_DOCS.CSH005}`
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const [dependency, type, action, from, to] = cells;
|
|
136
|
+
if (!dependency) onError({
|
|
137
|
+
lineNumber: row.startLine,
|
|
138
|
+
detail: `Dependencies table row has an empty 'Dependency' cell. See: ${RULE_DOCS.CSH005}`
|
|
139
|
+
});
|
|
140
|
+
if (!VALID_TYPES.has(type)) onError({
|
|
141
|
+
lineNumber: row.startLine,
|
|
142
|
+
detail: `Invalid dependency type '${type}'. Valid types are: ${[...VALID_TYPES].join(", ")}. See: ${RULE_DOCS.CSH005}`
|
|
143
|
+
});
|
|
144
|
+
if (!VALID_ACTIONS.has(action)) onError({
|
|
145
|
+
lineNumber: row.startLine,
|
|
146
|
+
detail: `Invalid dependency action '${action}'. Valid actions are: ${[...VALID_ACTIONS].join(", ")}. See: ${RULE_DOCS.CSH005}`
|
|
147
|
+
});
|
|
148
|
+
if (from && !VERSION_RE.test(from)) onError({
|
|
149
|
+
lineNumber: row.startLine,
|
|
150
|
+
detail: `Invalid 'from' value '${from}'. Must be a semver string or em dash (\u2014). See: ${RULE_DOCS.CSH005}`
|
|
151
|
+
});
|
|
152
|
+
if (to && !VERSION_RE.test(to)) onError({
|
|
153
|
+
lineNumber: row.startLine,
|
|
154
|
+
detail: `Invalid 'to' value '${to}'. Must be a semver string or em dash (\u2014). See: ${RULE_DOCS.CSH005}`
|
|
155
|
+
});
|
|
156
|
+
if (action === "added" && from !== EM_DASH) onError({
|
|
157
|
+
lineNumber: row.startLine,
|
|
158
|
+
detail: `'from' must be '\u2014' when action is 'added' (got '${from}'). See: ${RULE_DOCS.CSH005}`
|
|
159
|
+
});
|
|
160
|
+
if (action === "removed" && to !== EM_DASH) onError({
|
|
161
|
+
lineNumber: row.startLine,
|
|
162
|
+
detail: `'to' must be '\u2014' when action is 'removed' (got '${to}'). See: ${RULE_DOCS.CSH005}`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
export { DependencyTableFormatRule };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { RULE_DOCS } from "../../constants.js";
|
|
2
|
+
import { getHeadingLevel } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/markdownlint/rules/heading-hierarchy.ts
|
|
5
|
+
/**
|
|
6
|
+
* markdownlint rule: `changeset-heading-hierarchy` (CSH001).
|
|
7
|
+
*
|
|
8
|
+
* Validates heading structure in changeset markdown files by inspecting
|
|
9
|
+
* `atxHeading` micromark tokens for three constraints:
|
|
10
|
+
*
|
|
11
|
+
* 1. **No h1 headings** -- h1 is reserved for the version title generated by
|
|
12
|
+
* the changelog formatter.
|
|
13
|
+
* 2. **Start at h2** -- the first heading in a changeset must be h2.
|
|
14
|
+
* 3. **No depth skips** -- heading levels must increase sequentially
|
|
15
|
+
* (h2 then h3, not h2 then h4).
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* This rule mirrors the remark-lint rule `remarkLintHeadingHierarchy` but uses
|
|
19
|
+
* markdownlint's micromark token API so it can run inside markdownlint-cli2 and
|
|
20
|
+
* the VS Code markdownlint extension.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```json
|
|
24
|
+
* {
|
|
25
|
+
* "changeset-heading-hierarchy": true
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @see {@link https://github.com/savvy-web/changesets/blob/main/docs/rules/CSH001.md | CSH001 rule documentation}
|
|
30
|
+
* @see `src/remark/rules/heading-hierarchy.ts` for the corresponding remark-lint rule
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
const HeadingHierarchyRule = {
|
|
35
|
+
names: ["changeset-heading-hierarchy", "CSH001"],
|
|
36
|
+
description: "Heading hierarchy must start at h2 with no h1 or depth skips",
|
|
37
|
+
tags: ["changeset"],
|
|
38
|
+
parser: "micromark",
|
|
39
|
+
function: function CSH001(params, onError) {
|
|
40
|
+
let prevDepth = 0;
|
|
41
|
+
for (const token of params.parsers.micromark.tokens) {
|
|
42
|
+
if (token.type !== "atxHeading") continue;
|
|
43
|
+
const depth = getHeadingLevel(token);
|
|
44
|
+
if (depth === 1) {
|
|
45
|
+
onError({
|
|
46
|
+
lineNumber: token.startLine,
|
|
47
|
+
detail: `h1 headings are not allowed in changeset files. Use h2 (##) for top-level sections like "## Features" or "## Bug Fixes". h1 is reserved for the version title generated by the changelog formatter. See: ${RULE_DOCS.CSH001}`
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (prevDepth > 0 && depth > prevDepth + 1) onError({
|
|
52
|
+
lineNumber: token.startLine,
|
|
53
|
+
detail: `Heading level skipped: expected h${prevDepth + 1} or lower, found h${depth}. Headings must increase sequentially (h2 → h3 → h4). Add the missing intermediate level or reduce this heading's depth. See: ${RULE_DOCS.CSH001}`
|
|
54
|
+
});
|
|
55
|
+
prevDepth = depth;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//#endregion
|
|
61
|
+
export { HeadingHierarchyRule };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { allHeadings, isValidHeading } from "../../categories/index.js";
|
|
2
|
+
import { RULE_DOCS } from "../../constants.js";
|
|
3
|
+
import { getHeadingLevel, getHeadingText } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
//#region src/changesets/markdownlint/rules/required-sections.ts
|
|
6
|
+
/**
|
|
7
|
+
* markdownlint rule: `changeset-required-sections` (CSH002).
|
|
8
|
+
*
|
|
9
|
+
* Validates that every h2 (`atxHeading` with depth 2) in a changeset markdown
|
|
10
|
+
* file matches a known category heading from the category system. When an
|
|
11
|
+
* unrecognized heading is found, the error detail lists all valid headings.
|
|
12
|
+
*
|
|
13
|
+
* @remarks
|
|
14
|
+
* Heading comparison is case-insensitive. The set of valid headings is provided
|
|
15
|
+
* by `allHeadings()` and `isValidHeading()` from the category system
|
|
16
|
+
* (`src/categories/index.ts`). This rule inspects `atxHeading` micromark
|
|
17
|
+
* tokens and extracts their text via the {@link getHeadingText} utility.
|
|
18
|
+
*
|
|
19
|
+
* This rule mirrors the remark-lint rule `remarkLintRequiredSections` but uses
|
|
20
|
+
* markdownlint's micromark token API so it can run inside markdownlint-cli2 and
|
|
21
|
+
* the VS Code markdownlint extension.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```json
|
|
25
|
+
* {
|
|
26
|
+
* "changeset-required-sections": true
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @see {@link https://github.com/savvy-web/changesets/blob/main/docs/rules/CSH002.md | CSH002 rule documentation}
|
|
31
|
+
* @see `src/remark/rules/required-sections.ts` for the corresponding remark-lint rule
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
const RequiredSectionsRule = {
|
|
36
|
+
names: ["changeset-required-sections", "CSH002"],
|
|
37
|
+
description: "Section headings must match known changeset categories",
|
|
38
|
+
tags: ["changeset"],
|
|
39
|
+
parser: "micromark",
|
|
40
|
+
function: function CSH002(params, onError) {
|
|
41
|
+
for (const token of params.parsers.micromark.tokens) {
|
|
42
|
+
if (token.type !== "atxHeading") continue;
|
|
43
|
+
if (getHeadingLevel(token) !== 2) continue;
|
|
44
|
+
const text = getHeadingText(token);
|
|
45
|
+
if (!isValidHeading(text)) onError({
|
|
46
|
+
lineNumber: token.startLine,
|
|
47
|
+
detail: `Unknown section "${text}". Valid h2 headings are: ${allHeadings().join(", ")}. Heading comparison is case-insensitive. See: ${RULE_DOCS.CSH002}`
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { RequiredSectionsRule };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { RULE_DOCS } from "../../constants.js";
|
|
2
|
+
import { getHeadingLevel } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/markdownlint/rules/uncategorized-content.ts
|
|
5
|
+
/**
|
|
6
|
+
* markdownlint rule: `changeset-uncategorized-content` (CSH004).
|
|
7
|
+
*
|
|
8
|
+
* Detects content that appears before the first h2 heading in a changeset
|
|
9
|
+
* markdown file. All substantive content must be placed under a categorized
|
|
10
|
+
* section (`## heading`).
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* The rule iterates over the top-level micromark token stream and stops at the
|
|
14
|
+
* first `atxHeading` with depth 2. Any token encountered before that heading
|
|
15
|
+
* that is not a `lineEnding`, `lineEndingBlank`, or `htmlFlow` (HTML comments)
|
|
16
|
+
* triggers an error. This ensures that changeset content is always grouped
|
|
17
|
+
* under a recognized category heading.
|
|
18
|
+
*
|
|
19
|
+
* This rule mirrors the remark-lint rule `remarkLintUncategorizedContent` but
|
|
20
|
+
* uses markdownlint's micromark token API so it can run inside
|
|
21
|
+
* markdownlint-cli2 and the VS Code markdownlint extension.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```json
|
|
25
|
+
* {
|
|
26
|
+
* "changeset-uncategorized-content": true
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @see {@link https://github.com/savvy-web/changesets/blob/main/docs/rules/CSH004.md | CSH004 rule documentation}
|
|
31
|
+
* @see `src/remark/rules/uncategorized-content.ts` for the corresponding remark-lint rule
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
const UncategorizedContentRule = {
|
|
36
|
+
names: ["changeset-uncategorized-content", "CSH004"],
|
|
37
|
+
description: "All content must be placed under a category heading (## heading)",
|
|
38
|
+
tags: ["changeset"],
|
|
39
|
+
parser: "micromark",
|
|
40
|
+
function: function CSH004(params, onError) {
|
|
41
|
+
const tokens = params.parsers.micromark.tokens;
|
|
42
|
+
for (const token of tokens) {
|
|
43
|
+
if (token.type === "atxHeading" && getHeadingLevel(token) === 2) break;
|
|
44
|
+
if (token.type === "lineEnding" || token.type === "lineEndingBlank" || token.type === "htmlFlow") continue;
|
|
45
|
+
if (token.type !== "atxHeading") onError({
|
|
46
|
+
lineNumber: token.startLine,
|
|
47
|
+
detail: `Content must be placed under a category heading (## heading). Move this content under an appropriate section like "## Features" or "## Bug Fixes". If it doesn't fit an existing category, use "## Other". See: ${RULE_DOCS.CSH004}`
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { UncategorizedContentRule };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RULE_DOCS } from "../../constants.js";
|
|
2
|
+
|
|
3
|
+
//#region src/changesets/markdownlint/rules/utils.ts
|
|
4
|
+
/**
|
|
5
|
+
* Get the heading level (1-6) from an `atxHeading` token.
|
|
6
|
+
*
|
|
7
|
+
* @param heading - The `atxHeading` micromark token
|
|
8
|
+
* @returns The heading depth (number of `#` characters), or 0 if no sequence found
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
function getHeadingLevel(heading) {
|
|
13
|
+
const sequence = heading.children.find((c) => c.type === "atxHeadingSequence");
|
|
14
|
+
return sequence ? sequence.text.length : 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get the plain text content of an `atxHeading` token.
|
|
18
|
+
*
|
|
19
|
+
* @param heading - The `atxHeading` micromark token
|
|
20
|
+
* @returns The heading text, or empty string if no text token found
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
function getHeadingText(heading) {
|
|
25
|
+
const textToken = heading.children.find((c) => c.type === "atxHeadingText");
|
|
26
|
+
return textToken ? textToken.text : "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { getHeadingLevel, getHeadingText };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { collapseDependencyRows, parseDependencyTable, serializeDependencyTable, sortDependencyRows } from "../../utils/dependency-table.js";
|
|
2
|
+
import { getBlockSections, getHeadingText, getVersionBlocks } from "../../utils/version-blocks.js";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/remark/plugins/aggregate-dependency-tables.ts
|
|
5
|
+
const AggregateDependencyTablesPlugin = () => {
|
|
6
|
+
return (tree) => {
|
|
7
|
+
const blocks = getVersionBlocks(tree);
|
|
8
|
+
for (let b = blocks.length - 1; b >= 0; b--) {
|
|
9
|
+
const depSections = getBlockSections(tree, blocks[b]).filter((s) => getHeadingText(s.heading).toLowerCase() === "dependencies");
|
|
10
|
+
if (depSections.length === 0) continue;
|
|
11
|
+
const allRows = [];
|
|
12
|
+
const legacyContent = [];
|
|
13
|
+
for (const section of depSections) for (const node of section.contentNodes) if (node.type === "table") try {
|
|
14
|
+
const rows = parseDependencyTable(node);
|
|
15
|
+
allRows.push(...rows);
|
|
16
|
+
} catch {
|
|
17
|
+
legacyContent.push(node);
|
|
18
|
+
}
|
|
19
|
+
else legacyContent.push(node);
|
|
20
|
+
const collapsed = sortDependencyRows(collapseDependencyRows(allRows));
|
|
21
|
+
const indicesToRemove = [];
|
|
22
|
+
for (const section of depSections) {
|
|
23
|
+
indicesToRemove.push(section.headingIndex);
|
|
24
|
+
for (let c = 0; c < section.contentNodes.length; c++) indicesToRemove.push(section.headingIndex + 1 + c);
|
|
25
|
+
}
|
|
26
|
+
indicesToRemove.sort((a, b) => b - a);
|
|
27
|
+
for (const idx of indicesToRemove) tree.children.splice(idx, 1);
|
|
28
|
+
if (collapsed.length === 0 && legacyContent.length === 0) continue;
|
|
29
|
+
const insertAt = depSections[0].headingIndex;
|
|
30
|
+
const newNodes = [];
|
|
31
|
+
newNodes.push({
|
|
32
|
+
type: "heading",
|
|
33
|
+
depth: 3,
|
|
34
|
+
children: [{
|
|
35
|
+
type: "text",
|
|
36
|
+
value: "Dependencies"
|
|
37
|
+
}]
|
|
38
|
+
});
|
|
39
|
+
if (collapsed.length > 0) newNodes.push(serializeDependencyTable(collapsed));
|
|
40
|
+
newNodes.push(...legacyContent);
|
|
41
|
+
tree.children.splice(insertAt, 0, ...newNodes);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { AggregateDependencyTablesPlugin };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getVersionBlocks } from "../../utils/version-blocks.js";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/remark/plugins/contributor-footnotes.ts
|
|
5
|
+
/**
|
|
6
|
+
* Pattern matching `Thanks @user!` at end of text.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
const ATTRIBUTION_PLAIN_RE = /\s*Thanks @(\w[\w-]*)!$/;
|
|
11
|
+
/**
|
|
12
|
+
* Try to extract a linked attribution from the end of a paragraph's children.
|
|
13
|
+
* Pattern: text "...Thanks " + link "\@user" + text "!"
|
|
14
|
+
*
|
|
15
|
+
* @param children - The phrasing content children of a paragraph node
|
|
16
|
+
* @returns The contributor and the index to start removal from, or `undefined`
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
function extractLinkedAttribution(children) {
|
|
21
|
+
if (children.length < 3) return void 0;
|
|
22
|
+
const last = children[children.length - 1];
|
|
23
|
+
const secondLast = children[children.length - 2];
|
|
24
|
+
const thirdLast = children[children.length - 3];
|
|
25
|
+
if (last.type !== "text" || last.value !== "!") return void 0;
|
|
26
|
+
if (secondLast.type !== "link") return void 0;
|
|
27
|
+
const linkNode = secondLast;
|
|
28
|
+
if (linkNode.children.length !== 1 || linkNode.children[0].type !== "text") return void 0;
|
|
29
|
+
const linkText = linkNode.children[0].value;
|
|
30
|
+
if (!linkText.startsWith("@")) return void 0;
|
|
31
|
+
const username = linkText.slice(1);
|
|
32
|
+
if (thirdLast.type !== "text") return void 0;
|
|
33
|
+
const textNode = thirdLast;
|
|
34
|
+
if (!/\s*Thanks $/.test(textNode.value)) return void 0;
|
|
35
|
+
return {
|
|
36
|
+
contributor: {
|
|
37
|
+
username,
|
|
38
|
+
url: linkNode.url
|
|
39
|
+
},
|
|
40
|
+
removeFrom: children.length - 3
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const ContributorFootnotesPlugin = () => {
|
|
44
|
+
return (tree) => {
|
|
45
|
+
const blocks = getVersionBlocks(tree);
|
|
46
|
+
for (let b = blocks.length - 1; b >= 0; b--) {
|
|
47
|
+
const block = blocks[b];
|
|
48
|
+
const contributors = /* @__PURE__ */ new Map();
|
|
49
|
+
for (let i = block.startIndex; i < block.endIndex; i++) {
|
|
50
|
+
const node = tree.children[i];
|
|
51
|
+
if (node.type !== "list") continue;
|
|
52
|
+
visit(node, "paragraph", (para) => {
|
|
53
|
+
const linked = extractLinkedAttribution(para.children);
|
|
54
|
+
if (linked) {
|
|
55
|
+
const key = linked.contributor.username.toLowerCase();
|
|
56
|
+
if (!contributors.has(key)) contributors.set(key, linked.contributor);
|
|
57
|
+
const textNode = para.children[linked.removeFrom];
|
|
58
|
+
textNode.value = textNode.value.replace(/\s*Thanks $/, "");
|
|
59
|
+
para.children.splice(linked.removeFrom + 1, 2);
|
|
60
|
+
if (textNode.value === "") para.children.splice(linked.removeFrom, 1);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const last = para.children[para.children.length - 1];
|
|
64
|
+
if (last?.type === "text") {
|
|
65
|
+
const textNode = last;
|
|
66
|
+
const match = textNode.value.match(ATTRIBUTION_PLAIN_RE);
|
|
67
|
+
if (match) {
|
|
68
|
+
const username = match[1];
|
|
69
|
+
const key = username.toLowerCase();
|
|
70
|
+
if (!contributors.has(key)) contributors.set(key, {
|
|
71
|
+
username,
|
|
72
|
+
url: void 0
|
|
73
|
+
});
|
|
74
|
+
textNode.value = textNode.value.replace(ATTRIBUTION_PLAIN_RE, "");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (contributors.size === 0) continue;
|
|
80
|
+
const sorted = [...contributors.values()].sort((a, b) => a.username.toLowerCase().localeCompare(b.username.toLowerCase()));
|
|
81
|
+
const phrasingChildren = [];
|
|
82
|
+
phrasingChildren.push({
|
|
83
|
+
type: "text",
|
|
84
|
+
value: "Thanks to "
|
|
85
|
+
});
|
|
86
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
87
|
+
const contrib = sorted[i];
|
|
88
|
+
if (i > 0 && sorted.length > 2) phrasingChildren.push({
|
|
89
|
+
type: "text",
|
|
90
|
+
value: ", "
|
|
91
|
+
});
|
|
92
|
+
if (i > 0 && i === sorted.length - 1) phrasingChildren.push({
|
|
93
|
+
type: "text",
|
|
94
|
+
value: sorted.length === 2 ? " and " : "and "
|
|
95
|
+
});
|
|
96
|
+
if (contrib.url) phrasingChildren.push({
|
|
97
|
+
type: "link",
|
|
98
|
+
url: contrib.url,
|
|
99
|
+
children: [{
|
|
100
|
+
type: "text",
|
|
101
|
+
value: `@${contrib.username}`
|
|
102
|
+
}]
|
|
103
|
+
});
|
|
104
|
+
else phrasingChildren.push({
|
|
105
|
+
type: "text",
|
|
106
|
+
value: `@${contrib.username}`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
phrasingChildren.push({
|
|
110
|
+
type: "text",
|
|
111
|
+
value: " for their contributions!"
|
|
112
|
+
});
|
|
113
|
+
const paragraph = {
|
|
114
|
+
type: "paragraph",
|
|
115
|
+
children: phrasingChildren
|
|
116
|
+
};
|
|
117
|
+
tree.children.splice(block.endIndex, 0, paragraph);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
//#endregion
|
|
123
|
+
export { ContributorFootnotesPlugin };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getBlockSections, getVersionBlocks } from "../../utils/version-blocks.js";
|
|
2
|
+
import { toString } from "mdast-util-to-string";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/remark/plugins/deduplicate-items.ts
|
|
5
|
+
const DeduplicateItemsPlugin = () => {
|
|
6
|
+
return (tree) => {
|
|
7
|
+
const blocks = getVersionBlocks(tree);
|
|
8
|
+
for (const block of blocks) {
|
|
9
|
+
const sections = getBlockSections(tree, block);
|
|
10
|
+
for (const section of sections) for (const node of section.contentNodes) {
|
|
11
|
+
if (node.type !== "list") continue;
|
|
12
|
+
const list = node;
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
list.children = list.children.filter((item) => {
|
|
15
|
+
const text = toString(item);
|
|
16
|
+
if (seen.has(text)) return false;
|
|
17
|
+
seen.add(text);
|
|
18
|
+
return true;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
tree.children = tree.children.filter((node) => {
|
|
23
|
+
if (node.type === "list") return node.children.length > 0;
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { DeduplicateItemsPlugin };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getVersionBlocks } from "../../utils/version-blocks.js";
|
|
2
|
+
import { SKIP, visit } from "unist-util-visit";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/remark/plugins/issue-link-refs.ts
|
|
5
|
+
/**
|
|
6
|
+
* Pattern matching issue numbers like `#123`.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
const ISSUE_RE = /^#\d+$/;
|
|
11
|
+
const IssueLinkRefsPlugin = () => {
|
|
12
|
+
return (tree) => {
|
|
13
|
+
const blocks = getVersionBlocks(tree);
|
|
14
|
+
for (let b = blocks.length - 1; b >= 0; b--) {
|
|
15
|
+
const block = blocks[b];
|
|
16
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
17
|
+
for (let i = block.startIndex; i < block.endIndex; i++) {
|
|
18
|
+
const node = tree.children[i];
|
|
19
|
+
visit(node, "link", (linkNode, index, parent) => {
|
|
20
|
+
if (!parent || index === void 0) return;
|
|
21
|
+
if (linkNode.children.length !== 1 || linkNode.children[0].type !== "text") return;
|
|
22
|
+
const text = linkNode.children[0].value;
|
|
23
|
+
if (!ISSUE_RE.test(text)) return;
|
|
24
|
+
const label = text;
|
|
25
|
+
if (!definitions.has(label)) definitions.set(label, {
|
|
26
|
+
label,
|
|
27
|
+
url: linkNode.url
|
|
28
|
+
});
|
|
29
|
+
const ref = {
|
|
30
|
+
type: "linkReference",
|
|
31
|
+
identifier: label,
|
|
32
|
+
label,
|
|
33
|
+
referenceType: "full",
|
|
34
|
+
children: [{
|
|
35
|
+
type: "text",
|
|
36
|
+
value: label
|
|
37
|
+
}]
|
|
38
|
+
};
|
|
39
|
+
parent.children[index] = ref;
|
|
40
|
+
return SKIP;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (definitions.size === 0) continue;
|
|
44
|
+
const defNodes = [...definitions.values()].sort((a, b) => {
|
|
45
|
+
return Number.parseInt(a.label.slice(1), 10) - Number.parseInt(b.label.slice(1), 10);
|
|
46
|
+
}).map((def) => ({
|
|
47
|
+
type: "definition",
|
|
48
|
+
identifier: def.label,
|
|
49
|
+
label: def.label,
|
|
50
|
+
url: def.url
|
|
51
|
+
}));
|
|
52
|
+
tree.children.splice(block.endIndex, 0, ...defNodes);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { IssueLinkRefsPlugin };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fromHeading } from "../../categories/index.js";
|
|
2
|
+
import { getBlockSections, getHeadingText, getVersionBlocks } from "../../utils/version-blocks.js";
|
|
3
|
+
|
|
4
|
+
//#region src/changesets/remark/plugins/merge-sections.ts
|
|
5
|
+
const MergeSectionsPlugin = () => {
|
|
6
|
+
return (tree) => {
|
|
7
|
+
const blocks = getVersionBlocks(tree);
|
|
8
|
+
for (let b = blocks.length - 1; b >= 0; b--) {
|
|
9
|
+
const sections = getBlockSections(tree, blocks[b]);
|
|
10
|
+
if (sections.length <= 1) continue;
|
|
11
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12
|
+
for (let s = 0; s < sections.length; s++) {
|
|
13
|
+
const text = getHeadingText(sections[s].heading);
|
|
14
|
+
const category = fromHeading(text);
|
|
15
|
+
const key = category ? category.heading.toLowerCase() : text.toLowerCase();
|
|
16
|
+
const existing = groups.get(key);
|
|
17
|
+
if (existing) existing.push(s);
|
|
18
|
+
else groups.set(key, [s]);
|
|
19
|
+
}
|
|
20
|
+
const indicesToRemove = [];
|
|
21
|
+
for (const indices of groups.values()) {
|
|
22
|
+
if (indices.length <= 1) continue;
|
|
23
|
+
const target = sections[indices[0]];
|
|
24
|
+
const targetEndIndex = target.headingIndex + 1 + target.contentNodes.length;
|
|
25
|
+
const mergedContent = [];
|
|
26
|
+
for (let d = 1; d < indices.length; d++) {
|
|
27
|
+
const dup = sections[indices[d]];
|
|
28
|
+
mergedContent.push(...dup.contentNodes);
|
|
29
|
+
indicesToRemove.push(dup.headingIndex);
|
|
30
|
+
for (let c = 0; c < dup.contentNodes.length; c++) indicesToRemove.push(dup.headingIndex + 1 + c);
|
|
31
|
+
}
|
|
32
|
+
tree.children.splice(targetEndIndex, 0, ...mergedContent);
|
|
33
|
+
const shift = mergedContent.length;
|
|
34
|
+
for (let r = 0; r < indicesToRemove.length; r++) if (indicesToRemove[r] >= targetEndIndex) indicesToRemove[r] += shift;
|
|
35
|
+
}
|
|
36
|
+
indicesToRemove.sort((a, b) => b - a);
|
|
37
|
+
for (const idx of indicesToRemove) tree.children.splice(idx, 1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { MergeSectionsPlugin };
|