@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
|
@@ -9,6 +9,12 @@ import { visit } from "unist-util-visit";
|
|
|
9
9
|
*/
|
|
10
10
|
const ATTRIBUTION_PLAIN_RE = /\s*Thanks @(\w[\w-]*)!$/;
|
|
11
11
|
/**
|
|
12
|
+
* Pattern matching `@user` mentions inside an existing Thanks section.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
const MENTION_RE = /@(\w[\w-]*)/g;
|
|
17
|
+
/**
|
|
12
18
|
* Try to extract a linked attribution from the end of a paragraph's children.
|
|
13
19
|
* Pattern: text "...Thanks " + link "\@user" + text "!"
|
|
14
20
|
*
|
|
@@ -40,81 +46,287 @@ function extractLinkedAttribution(children) {
|
|
|
40
46
|
removeFrom: children.length - 3
|
|
41
47
|
};
|
|
42
48
|
}
|
|
43
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Strip a trailing attribution (linked or plain) from a paragraph, adding
|
|
51
|
+
* the contributor to the map.
|
|
52
|
+
*
|
|
53
|
+
* @param para - The paragraph to inspect and mutate
|
|
54
|
+
* @param contributors - The per-block contributor accumulator
|
|
55
|
+
* @returns `true` when an attribution was found and removed
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
function stripAttribution(para, contributors) {
|
|
60
|
+
const linked = extractLinkedAttribution(para.children);
|
|
61
|
+
if (linked) {
|
|
62
|
+
const key = linked.contributor.username.toLowerCase();
|
|
63
|
+
if (!contributors.has(key)) contributors.set(key, linked.contributor);
|
|
64
|
+
const textNode = para.children[linked.removeFrom];
|
|
65
|
+
textNode.value = textNode.value.replace(/\s*Thanks $/, "");
|
|
66
|
+
para.children.splice(linked.removeFrom + 1, 2);
|
|
67
|
+
if (textNode.value === "") para.children.splice(linked.removeFrom, 1);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const last = para.children[para.children.length - 1];
|
|
71
|
+
if (last?.type === "text") {
|
|
72
|
+
const textNode = last;
|
|
73
|
+
const match = textNode.value.match(ATTRIBUTION_PLAIN_RE);
|
|
74
|
+
if (match) {
|
|
75
|
+
const username = match[1];
|
|
76
|
+
const key = username.toLowerCase();
|
|
77
|
+
if (!contributors.has(key)) contributors.set(key, {
|
|
78
|
+
username,
|
|
79
|
+
url: void 0
|
|
80
|
+
});
|
|
81
|
+
textNode.value = textNode.value.replace(ATTRIBUTION_PLAIN_RE, "");
|
|
82
|
+
if (textNode.value === "") para.children.pop();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Harvest every `@user` mention (linked or plain) from a node subtree.
|
|
90
|
+
* Used to absorb an existing `### Thanks` section so re-running the plugin
|
|
91
|
+
* is idempotent.
|
|
92
|
+
*
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
function harvestMentions(node, contributors) {
|
|
96
|
+
visit(node, (child) => {
|
|
97
|
+
if (child.type === "link") {
|
|
98
|
+
const link = child;
|
|
99
|
+
const only = link.children.length === 1 ? link.children[0] : void 0;
|
|
100
|
+
if (only?.type === "text" && only.value.startsWith("@")) {
|
|
101
|
+
const username = only.value.slice(1);
|
|
102
|
+
const key = username.toLowerCase();
|
|
103
|
+
if (!contributors.has(key)) contributors.set(key, {
|
|
104
|
+
username,
|
|
105
|
+
url: link.url
|
|
106
|
+
});
|
|
107
|
+
return "skip";
|
|
108
|
+
}
|
|
109
|
+
} else if (child.type === "text") for (const match of child.value.matchAll(MENTION_RE)) {
|
|
110
|
+
const key = match[1].toLowerCase();
|
|
111
|
+
if (!contributors.has(key)) contributors.set(key, {
|
|
112
|
+
username: match[1],
|
|
113
|
+
url: void 0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the `Thanks to ... for their contributions!` summary paragraph.
|
|
120
|
+
*
|
|
121
|
+
* @internal
|
|
122
|
+
*/
|
|
123
|
+
function buildSummaryParagraph(contributors) {
|
|
124
|
+
const sorted = [...contributors.values()].sort((a, b) => a.username.toLowerCase().localeCompare(b.username.toLowerCase()));
|
|
125
|
+
const phrasingChildren = [];
|
|
126
|
+
phrasingChildren.push({
|
|
127
|
+
type: "text",
|
|
128
|
+
value: "Thanks to "
|
|
129
|
+
});
|
|
130
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
131
|
+
const contrib = sorted[i];
|
|
132
|
+
if (i > 0 && sorted.length > 2) phrasingChildren.push({
|
|
133
|
+
type: "text",
|
|
134
|
+
value: ", "
|
|
135
|
+
});
|
|
136
|
+
if (i > 0 && i === sorted.length - 1) phrasingChildren.push({
|
|
137
|
+
type: "text",
|
|
138
|
+
value: sorted.length === 2 ? " and " : "and "
|
|
139
|
+
});
|
|
140
|
+
if (contrib.url) phrasingChildren.push({
|
|
141
|
+
type: "link",
|
|
142
|
+
url: contrib.url,
|
|
143
|
+
children: [{
|
|
144
|
+
type: "text",
|
|
145
|
+
value: `@${contrib.username}`
|
|
146
|
+
}]
|
|
147
|
+
});
|
|
148
|
+
else phrasingChildren.push({
|
|
149
|
+
type: "text",
|
|
150
|
+
value: `@${contrib.username}`
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
phrasingChildren.push({
|
|
154
|
+
type: "text",
|
|
155
|
+
value: " for their contributions!"
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
type: "paragraph",
|
|
159
|
+
children: phrasingChildren
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Remove list items (recursively) whose content was emptied by attribution
|
|
164
|
+
* stripping — a bullet that held ONLY `Thanks @user!` must not survive as a
|
|
165
|
+
* bare `- ` husk. Runs in both the `thanks: true` and `thanks: false` paths,
|
|
166
|
+
* since stripping is unconditional.
|
|
167
|
+
*
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
function pruneEmptiedListItems(list) {
|
|
171
|
+
list.children = list.children.filter((item) => {
|
|
172
|
+
item.children = item.children.filter((child) => {
|
|
173
|
+
if (child.type === "paragraph") return child.children.length > 0;
|
|
174
|
+
if (child.type === "list") {
|
|
175
|
+
const nested = child;
|
|
176
|
+
pruneEmptiedListItems(nested);
|
|
177
|
+
return nested.children.length > 0;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
181
|
+
return item.children.length > 0;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Text form of a paragraph: concatenated text values, with single-text link
|
|
186
|
+
* children flattened to their label. Returns `undefined` when the paragraph
|
|
187
|
+
* holds anything other than text and simple text-labelled links.
|
|
188
|
+
*
|
|
189
|
+
* @internal
|
|
190
|
+
*/
|
|
191
|
+
function flattenToText(para) {
|
|
192
|
+
let out = "";
|
|
193
|
+
for (const child of para.children) {
|
|
194
|
+
if (child.type === "text") {
|
|
195
|
+
out += child.value;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (child.type === "link") {
|
|
199
|
+
const link = child;
|
|
200
|
+
const only = link.children.length === 1 ? link.children[0] : void 0;
|
|
201
|
+
if (only?.type === "text") {
|
|
202
|
+
out += only.value;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Pattern matching the plugin's own merged summary paragraph, e.g.
|
|
212
|
+
* `Thanks to @alice, @bob, and @carol for their contributions!`.
|
|
213
|
+
*
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
const MERGED_SUMMARY_RE = /^Thanks to @\w[\w-]*(?:(?:, | and |, and |,and )@\w[\w-]*)* for their contributions!$/;
|
|
217
|
+
/**
|
|
218
|
+
* Whether a paragraph consists ENTIRELY of attribution content the plugin
|
|
219
|
+
* owns: either its own merged `Thanks to ... for their contributions!`
|
|
220
|
+
* summary, or one or more trailing `Thanks @user!` / `Thanks [@user](url)!`
|
|
221
|
+
* attributions with nothing else. Checked against a clone so the real node
|
|
222
|
+
* is never mutated — a paragraph carrying anything beyond pure attribution
|
|
223
|
+
* is not the plugin's to touch.
|
|
224
|
+
*
|
|
225
|
+
* @internal
|
|
226
|
+
*/
|
|
227
|
+
function isPureAttributionParagraph(para) {
|
|
228
|
+
const text = flattenToText(para);
|
|
229
|
+
if (text !== void 0 && MERGED_SUMMARY_RE.test(text)) return true;
|
|
230
|
+
const clone = structuredClone(para);
|
|
231
|
+
const scratch = /* @__PURE__ */ new Map();
|
|
232
|
+
while (clone.children.length > 0 && stripAttribution(clone, scratch));
|
|
233
|
+
return clone.children.length === 0;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Whether a Thanks-section body node is a pure attribution shape the plugin
|
|
237
|
+
* may harvest and remove: an attribution-only paragraph, or a list whose
|
|
238
|
+
* every item holds only attribution-only paragraphs. Anything else
|
|
239
|
+
* (mixed prose+mention, plain-name lists, code blocks, arbitrary prose) is
|
|
240
|
+
* preserved untouched and never mined for mentions.
|
|
241
|
+
*
|
|
242
|
+
* @internal
|
|
243
|
+
*/
|
|
244
|
+
function isPureAttributionNode(node) {
|
|
245
|
+
if (node.type === "paragraph") return isPureAttributionParagraph(node);
|
|
246
|
+
if (node.type === "list") {
|
|
247
|
+
const list = node;
|
|
248
|
+
return list.children.length > 0 && list.children.every((item) => item.children.length > 0 && item.children.every((child) => child.type === "paragraph" && isPureAttributionParagraph(child)));
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Whether a node is a depth-3 heading whose text is `Thanks`.
|
|
254
|
+
*
|
|
255
|
+
* @internal
|
|
256
|
+
*/
|
|
257
|
+
function isThanksHeading(node) {
|
|
258
|
+
if (node.type !== "heading" || node.depth !== 3) return false;
|
|
259
|
+
const heading = node;
|
|
260
|
+
const only = heading.children.length === 1 ? heading.children[0] : void 0;
|
|
261
|
+
return only?.type === "text" && only.value.trim().toLowerCase() === "thanks";
|
|
262
|
+
}
|
|
263
|
+
const ContributorFootnotesPlugin = (options) => {
|
|
264
|
+
const emitThanks = options?.thanks !== false;
|
|
44
265
|
return (tree) => {
|
|
45
266
|
const blocks = getVersionBlocks(tree);
|
|
46
267
|
for (let b = blocks.length - 1; b >= 0; b--) {
|
|
47
268
|
const block = blocks[b];
|
|
48
269
|
const contributors = /* @__PURE__ */ new Map();
|
|
270
|
+
const indicesToRemove = [];
|
|
271
|
+
let preservedThanksAnchor;
|
|
49
272
|
for (let i = block.startIndex; i < block.endIndex; i++) {
|
|
50
273
|
const node = tree.children[i];
|
|
51
|
-
if (node.type
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
274
|
+
if (node.type === "list") {
|
|
275
|
+
const listNode = node;
|
|
276
|
+
visit(listNode, "paragraph", (para) => {
|
|
277
|
+
stripAttribution(para, contributors);
|
|
278
|
+
});
|
|
279
|
+
pruneEmptiedListItems(listNode);
|
|
280
|
+
if (listNode.children.length === 0) indicesToRemove.push(i);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (node.type === "paragraph") {
|
|
284
|
+
const para = node;
|
|
285
|
+
if (stripAttribution(para, contributors) && para.children.length === 0) indicesToRemove.push(i);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (isThanksHeading(node)) {
|
|
289
|
+
const headingIndex = i;
|
|
290
|
+
const harvested = [];
|
|
291
|
+
let lastPreserved;
|
|
292
|
+
let j = i + 1;
|
|
293
|
+
for (; j < block.endIndex; j++) {
|
|
294
|
+
const contentNode = tree.children[j];
|
|
295
|
+
if (contentNode.type === "heading" && [2, 3].includes(contentNode.depth)) break;
|
|
296
|
+
if (contentNode.type === "definition") continue;
|
|
297
|
+
if (isPureAttributionNode(contentNode)) {
|
|
298
|
+
harvestMentions(contentNode, contributors);
|
|
299
|
+
harvested.push(j);
|
|
300
|
+
} else lastPreserved = contentNode;
|
|
62
301
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
}
|
|
302
|
+
if (lastPreserved === void 0) indicesToRemove.push(headingIndex, ...harvested);
|
|
303
|
+
else {
|
|
304
|
+
indicesToRemove.push(...harvested);
|
|
305
|
+
preservedThanksAnchor ??= lastPreserved;
|
|
76
306
|
}
|
|
77
|
-
|
|
307
|
+
i = j - 1;
|
|
308
|
+
}
|
|
78
309
|
}
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
});
|
|
310
|
+
const uniqueIndices = [...new Set(indicesToRemove)].sort((a, b) => b - a);
|
|
311
|
+
for (const idx of uniqueIndices) tree.children.splice(idx, 1);
|
|
312
|
+
if (!emitThanks || contributors.size === 0) continue;
|
|
313
|
+
if (preservedThanksAnchor !== void 0) {
|
|
314
|
+
const anchorIndex = tree.children.indexOf(preservedThanksAnchor);
|
|
315
|
+
if (anchorIndex !== -1) {
|
|
316
|
+
tree.children.splice(anchorIndex + 1, 0, buildSummaryParagraph(contributors));
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
108
319
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
320
|
+
let insertAt = block.endIndex - uniqueIndices.length;
|
|
321
|
+
while (insertAt > block.startIndex && tree.children[insertAt - 1]?.type === "definition") insertAt--;
|
|
322
|
+
tree.children.splice(insertAt, 0, {
|
|
323
|
+
type: "heading",
|
|
324
|
+
depth: 3,
|
|
325
|
+
children: [{
|
|
326
|
+
type: "text",
|
|
327
|
+
value: "Thanks"
|
|
328
|
+
}]
|
|
329
|
+
}, buildSummaryParagraph(contributors));
|
|
118
330
|
}
|
|
119
331
|
};
|
|
120
332
|
};
|
|
@@ -8,6 +8,23 @@ import { getBlockSections, getHeadingText, getVersionBlocks } from "../../utils/
|
|
|
8
8
|
* @internal
|
|
9
9
|
*/
|
|
10
10
|
const UNKNOWN_PRIORITY = 999;
|
|
11
|
+
/**
|
|
12
|
+
* Priority for the `Thanks` section emitted by {@link ContributorFootnotesPlugin}.
|
|
13
|
+
* Sorts after everything — including unknown headings — so the contributor
|
|
14
|
+
* credits always close the version block.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
const THANKS_PRIORITY = 1e3;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the sort priority for a section heading.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
function headingPriority(text) {
|
|
25
|
+
if (text.trim().toLowerCase() === "thanks") return THANKS_PRIORITY;
|
|
26
|
+
return fromHeading(text)?.priority ?? UNKNOWN_PRIORITY;
|
|
27
|
+
}
|
|
11
28
|
const ReorderSectionsPlugin = () => {
|
|
12
29
|
return (tree) => {
|
|
13
30
|
const blocks = getVersionBlocks(tree);
|
|
@@ -18,9 +35,7 @@ const ReorderSectionsPlugin = () => {
|
|
|
18
35
|
const preamble = [];
|
|
19
36
|
for (let i = block.startIndex; i < block.endIndex; i++) if (i < sections[0].headingIndex) preamble.push(tree.children[i]);
|
|
20
37
|
else break;
|
|
21
|
-
const sorted = [...sections].sort((a, b) =>
|
|
22
|
-
return (fromHeading(getHeadingText(a.heading))?.priority ?? UNKNOWN_PRIORITY) - (fromHeading(getHeadingText(b.heading))?.priority ?? UNKNOWN_PRIORITY);
|
|
23
|
-
});
|
|
38
|
+
const sorted = [...sections].sort((a, b) => headingPriority(getHeadingText(a.heading)) - headingPriority(getHeadingText(b.heading)));
|
|
24
39
|
if (sorted.every((s, i) => s.headingIndex === sections[i].headingIndex)) continue;
|
|
25
40
|
const newChildren = [...preamble];
|
|
26
41
|
for (const section of sorted) newChildren.push(section.heading, ...section.contentNodes);
|
|
@@ -3,8 +3,8 @@ import { DependencyTableFormatRule } from "./rules/dependency-table-format.js";
|
|
|
3
3
|
import { HeadingHierarchyRule } from "./rules/heading-hierarchy.js";
|
|
4
4
|
import { RequiredSectionsRule } from "./rules/required-sections.js";
|
|
5
5
|
import { UncategorizedContentRule } from "./rules/uncategorized-content.js";
|
|
6
|
-
import { AggregateDependencyTablesPlugin } from "./plugins/aggregate-dependency-tables.js";
|
|
7
6
|
import { ContributorFootnotesPlugin } from "./plugins/contributor-footnotes.js";
|
|
7
|
+
import { AggregateDependencyTablesPlugin } from "./plugins/aggregate-dependency-tables.js";
|
|
8
8
|
import { DeduplicateItemsPlugin } from "./plugins/deduplicate-items.js";
|
|
9
9
|
import { IssueLinkRefsPlugin } from "./plugins/issue-link-refs.js";
|
|
10
10
|
import { MergeSectionsPlugin } from "./plugins/merge-sections.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseDependencyTable } from "../../utils/dependency-table.js";
|
|
2
2
|
import { RULE_DOCS } from "../../constants.js";
|
|
3
|
+
import { scanDependencySection } from "../../utils/dependency-section.js";
|
|
3
4
|
import { toString } from "mdast-util-to-string";
|
|
4
5
|
import { lintRule } from "unified-lint-rule";
|
|
5
6
|
import { visit } from "unist-util-visit";
|
|
@@ -12,18 +13,15 @@ const DependencyTableFormatRule = lintRule("remark-lint:changeset-dependency-tab
|
|
|
12
13
|
if (node.depth !== 2) return;
|
|
13
14
|
if (toString(node).toLowerCase() !== "dependencies") return;
|
|
14
15
|
if (index === void 0) return;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
const tables = content.filter((n) => n.type === "table");
|
|
22
|
-
if (tables.length === 0) {
|
|
16
|
+
const scan = scanDependencySection(tree.children, index + 1, {
|
|
17
|
+
isHeading: (n) => n.type === "heading",
|
|
18
|
+
isTable: (n) => n.type === "table"
|
|
19
|
+
});
|
|
20
|
+
if (scan.table === void 0) {
|
|
23
21
|
file.message(`Dependencies section must contain a table, not a list or paragraph. See: ${RULE_DOCS.CSH005}`, node);
|
|
24
22
|
return;
|
|
25
23
|
}
|
|
26
|
-
const table =
|
|
24
|
+
const table = scan.table;
|
|
27
25
|
try {
|
|
28
26
|
const rows = parseDependencyTable(table);
|
|
29
27
|
for (const row of rows) {
|
|
@@ -52,8 +52,14 @@ const DependencyActionSchema = Schema.Literals([
|
|
|
52
52
|
* @remarks
|
|
53
53
|
* Unlike {@link DependencyTypeSchema} (which uses plural npm field names like
|
|
54
54
|
* `"dependencies"`), this schema uses singular forms (`"dependency"`) and adds
|
|
55
|
-
*
|
|
56
|
-
* `"
|
|
55
|
+
* four additional types with no `package.json` dependency-field counterpart:
|
|
56
|
+
* `"workspace"` for monorepo workspace references, `"config"` for
|
|
57
|
+
* configuration toolchain updates (e.g., ESLint, TypeScript), `"runtime"` for
|
|
58
|
+
* language-runtime upgrades (e.g., the Node.js engine itself), and
|
|
59
|
+
* `"packageManager"` for the package manager's own self-upgrade (pnpm, bun,
|
|
60
|
+
* npm). `"runtime"` and `"packageManager"` are release-neutral — like
|
|
61
|
+
* `devDependency` rows they document toolchain movement without implying a
|
|
62
|
+
* consumer-facing version bump.
|
|
57
63
|
*
|
|
58
64
|
* @example
|
|
59
65
|
* ```typescript
|
|
@@ -76,7 +82,9 @@ const DependencyTableTypeSchema = Schema.Literals([
|
|
|
76
82
|
"peerDependency",
|
|
77
83
|
"optionalDependency",
|
|
78
84
|
"workspace",
|
|
79
|
-
"config"
|
|
85
|
+
"config",
|
|
86
|
+
"runtime",
|
|
87
|
+
"packageManager"
|
|
80
88
|
]);
|
|
81
89
|
/**
|
|
82
90
|
* The canonical accepted-value pattern for a dependency-table From/To cell:
|
|
@@ -106,6 +106,14 @@ const ChangesetOptionsSchema = Schema.Struct({
|
|
|
106
106
|
/** Custom issue reference prefixes (e.g., `["#", "GH-"]`). */
|
|
107
107
|
issuePrefixes: Schema.optional(Schema.Array(Schema.String)),
|
|
108
108
|
/**
|
|
109
|
+
* Whether to credit contributors with `Thanks \@user!` attributions
|
|
110
|
+
* (aggregated into a `### Thanks` section by the transform pipeline).
|
|
111
|
+
* Defaults to `true`; set `false` to strip attribution entirely —
|
|
112
|
+
* no inline thanks and no Thanks section. PR references are kept
|
|
113
|
+
* either way.
|
|
114
|
+
*/
|
|
115
|
+
thanks: Schema.optional(Schema.Boolean),
|
|
116
|
+
/**
|
|
109
117
|
* Per-package release surfaces. Each entry declares `additionalScopes`
|
|
110
118
|
* (globs outside the workspace dir that belong to the package) and
|
|
111
119
|
* `versionFiles` (files bumped in lockstep with the package's version).
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ConfigurationError } from "../errors.js";
|
|
2
2
|
import { ChangesetOptionsSchema } from "../schemas/options.js";
|
|
3
|
+
import { MARKDOWNLINT_CONFIG_PATH } from "../../lint/cli/sections.js";
|
|
3
4
|
import { ChangesetConfigReader } from "../../services/ChangesetConfigReader.js";
|
|
4
5
|
import { SilkPublishability, readTargetsBinding } from "../../services/SilkPublishability.js";
|
|
5
6
|
import { Context, Effect, FileSystem, Layer, Path, Result, Schema } from "effect";
|
|
6
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
8
|
import { GlobPattern, GlobPatternOptions } from "@effected/glob";
|
|
8
9
|
import { compileAndExpand } from "@effected/walker";
|
|
9
10
|
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
@@ -78,6 +79,10 @@ const ClassificationReasonSchema = Schema.Union([
|
|
|
78
79
|
kind: Schema.Literal("versionFile"),
|
|
79
80
|
glob: Schema.String
|
|
80
81
|
}),
|
|
82
|
+
Schema.Struct({
|
|
83
|
+
kind: Schema.Literal("unmappedHint"),
|
|
84
|
+
hint: Schema.String.annotate({ description: "Why this UNMAPPED path is probably not an unattributed change: it matches a config glob whose file no longer materializes (deleted versionFiles / additionalScopes target) or mirrors a known package template. The package stays null — a hint is context, not attribution." })
|
|
85
|
+
}),
|
|
81
86
|
Schema.Null
|
|
82
87
|
]).annotate({ identifier: "ClassificationReason" });
|
|
83
88
|
/** The result of classifying a single path against a resolved config. @public */
|
|
@@ -398,7 +403,7 @@ function makeShape(reader, discovery, fs) {
|
|
|
398
403
|
throw e;
|
|
399
404
|
}
|
|
400
405
|
const decodedOptions = yield* Schema.decodeUnknownEffect(ChangesetOptionsSchema)(normalized).pipe(Effect.mapError((parseError) => configErrorFromParseError(parseError, configPath)));
|
|
401
|
-
const workspaces = (yield* discovery.
|
|
406
|
+
const workspaces = (yield* discovery.listPackagesIn(projectDir).pipe(Effect.mapError((err) => new ConfigurationError({
|
|
402
407
|
field: "workspace",
|
|
403
408
|
reason: `Workspace discovery failed for ${projectDir}: ${err.message}`
|
|
404
409
|
})))).map((w) => ({
|
|
@@ -448,13 +453,43 @@ function makeShape(reader, discovery, fs) {
|
|
|
448
453
|
cache.clear();
|
|
449
454
|
yield* discovery.refresh();
|
|
450
455
|
});
|
|
456
|
+
const refreshIn = (directory) => Effect.gen(function* () {
|
|
457
|
+
const target = resolve(directory);
|
|
458
|
+
for (const key of [...cache.keys()]) if (target === key || target.startsWith(key + sep)) cache.delete(key);
|
|
459
|
+
yield* Effect.ignore(discovery.refreshIn(target));
|
|
460
|
+
});
|
|
451
461
|
return {
|
|
452
462
|
inspect,
|
|
453
463
|
classify,
|
|
454
|
-
refresh
|
|
464
|
+
refresh,
|
|
465
|
+
refreshIn
|
|
455
466
|
};
|
|
456
467
|
}
|
|
457
468
|
/**
|
|
469
|
+
* Known template-mirror pairs (#290): repo files generated from (or kept in
|
|
470
|
+
* byte-lockstep with) a template a package ships, keyed by projectDir-relative
|
|
471
|
+
* POSIX path. An unmapped diff on one of these is a mirror of an already
|
|
472
|
+
* attributed template change, not an unattributed change needing its own
|
|
473
|
+
* changeset — the hint spares the agent a manual diff. Deliberately a small
|
|
474
|
+
* static table, not a diff-correlation engine.
|
|
475
|
+
*/
|
|
476
|
+
const TEMPLATE_MIRRORS = { [MARKDOWNLINT_CONFIG_PATH]: "mirrors the @savvy-web/silk-effects markdownlint template (src/lint/cli/templates/markdownlint.gen.ts)" };
|
|
477
|
+
/**
|
|
478
|
+
* A machine-readable hint for a path that mapped to NO package (#290): a
|
|
479
|
+
* versionFiles / additionalScopes glob that names the path without a
|
|
480
|
+
* materialized file behind it (the deleted-file shape branch diffs produce),
|
|
481
|
+
* or a known template mirror. Pure — pattern matching only, no filesystem.
|
|
482
|
+
* `null` when nothing explains the path.
|
|
483
|
+
*/
|
|
484
|
+
function unmappedHint(inspected, rel) {
|
|
485
|
+
for (const s of inspected.packages) for (const vf of s.versionFiles) if (globMatchesRel(vf.glob, rel)) return `versionFiles of "${s.name}" (glob "${vf.glob}")`;
|
|
486
|
+
for (const s of inspected.packages) {
|
|
487
|
+
const glob = s.additionalScopes.find((g) => globMatchesRel(g, rel));
|
|
488
|
+
if (glob !== void 0) return `additionalScopes of "${s.name}" (glob "${glob}")`;
|
|
489
|
+
}
|
|
490
|
+
return Object.hasOwn(TEMPLATE_MIRRORS, rel) ? TEMPLATE_MIRRORS[rel] : null;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
458
493
|
* Classify a single path against an inspected config.
|
|
459
494
|
*/
|
|
460
495
|
function classifyOne(inspected, path) {
|
|
@@ -504,6 +539,17 @@ function classifyOne(inspected, path) {
|
|
|
504
539
|
package: rootScope,
|
|
505
540
|
reason: "workspace"
|
|
506
541
|
};
|
|
542
|
+
if (isInside(inspected.projectDir, abs)) {
|
|
543
|
+
const hint = unmappedHint(inspected, relative(inspected.projectDir, abs).replaceAll("\\", "/"));
|
|
544
|
+
if (hint !== null) return {
|
|
545
|
+
path,
|
|
546
|
+
package: null,
|
|
547
|
+
reason: {
|
|
548
|
+
kind: "unmappedHint",
|
|
549
|
+
hint
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
507
553
|
return {
|
|
508
554
|
path,
|
|
509
555
|
package: null,
|
|
@@ -524,7 +570,8 @@ function makeConfigInspectorTest(fixed) {
|
|
|
524
570
|
return Layer.succeed(ConfigInspector, {
|
|
525
571
|
inspect: () => Effect.succeed(fixed),
|
|
526
572
|
classify: (_cwd, paths) => Effect.succeed(paths.map((p) => classifyOne(fixed, p))),
|
|
527
|
-
refresh: () => Effect.void
|
|
573
|
+
refresh: () => Effect.void,
|
|
574
|
+
refreshIn: () => Effect.void
|
|
528
575
|
});
|
|
529
576
|
}
|
|
530
577
|
|