@heroiclands/package-build 25.1.2 → 25.2.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/CHANGELOG.md +36 -0
- package/assets/styles/content-base.css +0 -9
- package/bin/content-commands.mjs +22 -16
- package/bin/package-build.mjs +24 -25
- package/content-config.mjs +7 -34
- package/docs/api.md +38 -21
- package/docs/authoring/first-note.md +3 -3
- package/docs/authoring/frontmatter.md +30 -2
- package/docs/authoring/links-and-markup.md +92 -31
- package/docs/commands.md +103 -59
- package/docs/configuration.md +10 -45
- package/docs/content-format.md +7 -5
- package/docs/diagnostics.md +75 -0
- package/docs/encoding-core.md +11 -0
- package/docs/guides/book.md +3 -3
- package/docs/guides/module-project.md +1 -1
- package/docs/guides/reference-pages.md +7 -3
- package/docs/project-setup.md +3 -0
- package/docs/reference/format-details.md +329 -187
- package/docs/reference/note-types.md +274 -274
- package/engine/changelog-group.mjs +84 -173
- package/engine/changelog-labels.mjs +73 -0
- package/engine/changelog-lint.mjs +168 -44
- package/engine/content-expand.mjs +58 -38
- package/engine/content-format.yaml +0 -2
- package/engine/content-index.mjs +49 -23
- package/engine/content-links.mjs +4 -0
- package/engine/content-lint.mjs +2 -3
- package/engine/content-render.mjs +6 -2
- package/engine/culture-parents.mjs +1 -1
- package/engine/culture-reach.mjs +10 -1
- package/engine/diagnostics.mjs +3 -2
- package/engine/dictionary-rows.mjs +3 -1
- package/engine/encoding-lexicon.mjs +12 -2
- package/engine/first-use.mjs +301 -87
- package/engine/frontmatter-lint.mjs +40 -14
- package/engine/glossary-entries.mjs +5 -5
- package/engine/glossary-language.mjs +21 -44
- package/engine/homepage.mjs +0 -4
- package/engine/index-records.mjs +3 -1
- package/engine/infobox.mjs +6 -3
- package/engine/language-names.mjs +70 -10
- package/engine/language-rows.mjs +6 -7
- package/engine/lexical-sections.mjs +3 -1
- package/engine/literature-notes.mjs +4 -76
- package/engine/metadata-index.mjs +15 -8
- package/engine/name-forms.mjs +99 -0
- package/engine/name-refs.mjs +143 -0
- package/engine/name-rows.mjs +11 -1
- package/engine/names.mjs +27 -22
- package/engine/note-addresses.mjs +0 -3
- package/engine/note-frontmatter.mjs +3 -5
- package/engine/note-language.mjs +348 -0
- package/engine/note-vocabulary.mjs +35 -13
- package/engine/pdf-index.mjs +3 -2
- package/engine/pdf-render.mjs +2 -4
- package/engine/settlement-tables.mjs +4 -3
- package/engine/site-build.mjs +2 -7
- package/engine/site-search.mjs +3 -5
- package/engine/site-theme.mjs +1 -1
- package/engine/sql-tables.mjs +29 -14
- package/engine/wikilink-syntax.mjs +2 -0
- package/engine/word-citations.mjs +3 -3
- package/engine/word-rows.mjs +3 -1
- package/engine/word-terms.mjs +2 -2
- package/package.json +1 -1
- package/site-theme/static/css/style.css +0 -4
- package/types/engine/changelog-group.d.mts +6 -10
- package/types/engine/changelog-labels.d.mts +10 -0
- package/types/engine/changelog-lint.d.mts +11 -6
- package/types/engine/content-index.d.mts +16 -16
- package/types/engine/diagnostics.d.mts +1 -1
- package/types/engine/first-use.d.mts +97 -1
- package/types/engine/frontmatter-lint.d.mts +9 -2
- package/types/engine/glossary-language.d.mts +13 -6
- package/types/engine/homepage.d.mts +0 -3
- package/types/engine/language-names.d.mts +3 -0
- package/types/engine/literature-notes.d.mts +2 -27
- package/types/engine/name-forms.d.mts +61 -0
- package/types/engine/name-refs.d.mts +51 -0
- package/types/engine/name-rows.d.mts +4 -0
- package/types/engine/names.d.mts +12 -92
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-language.d.mts +47 -0
- package/types/engine/settlement-tables.d.mts +1 -1
- package/types/engine/site-build.d.mts +2 -6
|
@@ -12,29 +12,28 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Turn Changesets' output for the newest release into its final shape.
|
|
16
16
|
*
|
|
17
17
|
* `@heroiclands/package-build/changelog` (`changelog.cjs`) writes each
|
|
18
|
-
* changeset's summary into `CHANGELOG.md`
|
|
19
|
-
* `###
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* model `changelog check` reads a block's label from — rather than parsing
|
|
32
|
-
* the markdown a second way.
|
|
18
|
+
* changeset's summary into `CHANGELOG.md` verbatim, under a
|
|
19
|
+
* `### Major Changes`, `### Minor Changes` or `### Patch Changes` heading,
|
|
20
|
+
* one block per changeset in whatever order Changesets read the files. A
|
|
21
|
+
* release is read by subject, not by bump, so `groupChangelogText` removes
|
|
22
|
+
* every one of those headings, merges the blocks that share a label into one,
|
|
23
|
+
* and orders the labels as the changelog `labels` declares them. A label the list
|
|
24
|
+
* does not declare follows the declared ones, in order of first appearance,
|
|
25
|
+
* and `Miscellaneous`, which every repository has without declaring it, is
|
|
26
|
+
* always last.
|
|
27
|
+
*
|
|
28
|
+
* It reuses {@link module:engine/changelog-lint.topLevelBlocks}, the block
|
|
29
|
+
* model `changelog check` reads a block's label from, rather than parsing the
|
|
30
|
+
* markdown a second way.
|
|
33
31
|
*
|
|
34
32
|
* @module
|
|
35
33
|
*/
|
|
36
34
|
|
|
37
35
|
import {
|
|
36
|
+
MISCELLANEOUS,
|
|
38
37
|
codeLineSet,
|
|
39
38
|
lineColOf,
|
|
40
39
|
releaseSectionRange,
|
|
@@ -43,43 +42,13 @@ import {
|
|
|
43
42
|
} from "./changelog-lint.mjs";
|
|
44
43
|
|
|
45
44
|
/** A `### <Bump> Changes` heading — the generated scaffold, never authored. */
|
|
46
|
-
const CHANGES_HEADING_RE = /^### (?:Major|Minor|Patch) Changes
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A label, its complete opening paragraph and its remaining items.
|
|
50
|
-
*
|
|
51
|
-
* @param {string} blockText - One block, as {@link topLevelBlocks} collects
|
|
52
|
-
* it — its first line is the label line for a labelled block.
|
|
53
|
-
* @returns {{labelLine: string, introduction: string, rest: string}} The
|
|
54
|
-
* introduction retains hard wrapping and stops before a bullet or code.
|
|
55
|
-
*/
|
|
56
|
-
function splitLabelBlock(blockText) {
|
|
57
|
-
const lines = blockText.replace(/\s+$/, "").split("\n");
|
|
58
|
-
const labelLine = /^\*\*[^*]+\*\*/.exec(lines[0])[0];
|
|
59
|
-
const inline = lines[0].slice(labelLine.length).trimStart();
|
|
60
|
-
const introduction = inline ? [inline] : [];
|
|
61
|
-
const codeLines = codeLineSet(blockText);
|
|
62
|
-
let cursor = 1;
|
|
63
|
-
while (
|
|
64
|
-
cursor < lines.length &&
|
|
65
|
-
lines[cursor].trim() !== "" &&
|
|
66
|
-
!/^-\s/.test(lines[cursor]) &&
|
|
67
|
-
!codeLines.has(cursor + 1)
|
|
68
|
-
)
|
|
69
|
-
introduction.push(lines[cursor++]);
|
|
70
|
-
return {
|
|
71
|
-
labelLine,
|
|
72
|
-
introduction: introduction.join("\n"),
|
|
73
|
-
rest: lines.slice(cursor).join("\n").replace(/^\n+/, ""),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
45
|
+
const CHANGES_HEADING_RE = /^### (?:Major|Minor|Patch) Changes\s*$/;
|
|
76
46
|
|
|
77
47
|
/**
|
|
78
48
|
* A block's top-level bullets and the prose or code between them, in source
|
|
79
49
|
* order. A bullet retains its indented continuation and nested items.
|
|
80
50
|
*
|
|
81
|
-
* @param {string} text - A block's content, label line already stripped
|
|
82
|
-
* a lead block's whole text).
|
|
51
|
+
* @param {string} text - A block's content, label line already stripped.
|
|
83
52
|
* @returns {string[]}
|
|
84
53
|
*/
|
|
85
54
|
function itemsOf(text) {
|
|
@@ -111,7 +80,7 @@ function itemsOf(text) {
|
|
|
111
80
|
|
|
112
81
|
/**
|
|
113
82
|
* The first occurrence of each item, exact-duplicate text dropped, order
|
|
114
|
-
* preserved
|
|
83
|
+
* preserved.
|
|
115
84
|
*
|
|
116
85
|
* @param {string[]} items
|
|
117
86
|
* @returns {string[]}
|
|
@@ -129,66 +98,43 @@ function dedupeItems(items) {
|
|
|
129
98
|
}
|
|
130
99
|
|
|
131
100
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* already uses.
|
|
136
|
-
*
|
|
137
|
-
* @param {string[]} items
|
|
138
|
-
* @returns {string}
|
|
139
|
-
*/
|
|
140
|
-
function joinItems(items) {
|
|
141
|
-
if (items.length === 0) return "";
|
|
142
|
-
const allBullets = items.every((item) => /^-\s/.test(item));
|
|
143
|
-
return items.join(allBullets ? "\n" : "\n\n");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* One label's blocks (or every unlabelled one), folded into the single
|
|
148
|
-
* block `group` writes for it.
|
|
149
|
-
*
|
|
150
|
-
* A single block with no duplicate items retains its own spacing. Merged
|
|
151
|
-
* opening paragraphs form one hard-wrapped introduction before the items,
|
|
152
|
-
* so paragraph boundaries cannot turn labelled text into unlabelled lead.
|
|
101
|
+
* One label's blocks, folded into the single block `group` writes for it:
|
|
102
|
+
* the label line, a blank line, then every bullet in file order. Bullets are
|
|
103
|
+
* adjacent; any other item is separated by a blank line.
|
|
153
104
|
*
|
|
154
105
|
* @param {Array<{label: string|null, text: string}>} blocks - Every block
|
|
155
|
-
* sharing one label, in file order.
|
|
106
|
+
* sharing one label, in file order.
|
|
156
107
|
* @returns {string}
|
|
157
108
|
*/
|
|
158
109
|
function mergeGroup(blocks) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
110
|
+
const labelled = blocks[0].label !== null;
|
|
111
|
+
const items = dedupeItems(
|
|
112
|
+
blocks.flatMap((block) => {
|
|
113
|
+
if (!labelled) return itemsOf(block.text);
|
|
114
|
+
const [, ...rest] = block.text.replace(/\s+$/, "").split("\n");
|
|
115
|
+
return itemsOf(rest.join("\n"));
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
const joined = items.join(items.every((item) => /^-\s/.test(item)) ? "\n" : "\n\n");
|
|
119
|
+
if (!labelled) return joined;
|
|
120
|
+
const labelLine = blocks[0].text.split("\n")[0].trimEnd();
|
|
121
|
+
return joined ? `${labelLine}\n\n${joined}` : labelLine;
|
|
171
122
|
}
|
|
172
123
|
|
|
173
124
|
/**
|
|
174
|
-
* Where a group sorts
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* appeared in.
|
|
183
|
-
*
|
|
184
|
-
* @param {string|null} key - A group's label, or `null` for the lead group.
|
|
185
|
-
* @param {string[]} appearanceOrder - Every key, in the order its first
|
|
186
|
-
* block appeared in the section.
|
|
187
|
-
* @param {readonly string[]|null} labels - `changelog.labels`, or `null`.
|
|
125
|
+
* Where a group sorts in its release: an unlabelled block first, then the
|
|
126
|
+
* declared labels in their declared order, then any other label in order of
|
|
127
|
+
* first appearance, and `Miscellaneous` last. With no vocabulary declared,
|
|
128
|
+
* every label but `Miscellaneous` keeps its order of first appearance.
|
|
129
|
+
*
|
|
130
|
+
* @param {string|null} key - A group's label, or `null` for an unlabelled one.
|
|
131
|
+
* @param {string[]} appearanceOrder - Every key, in order of first appearance.
|
|
132
|
+
* @param {readonly string[]|null} labels - the changelog `labels`, or `null`.
|
|
188
133
|
* @returns {[number, number]} `[tier, rank]`, compared tier first.
|
|
189
134
|
*/
|
|
190
135
|
function groupRank(key, appearanceOrder, labels) {
|
|
191
136
|
if (key === null) return [-1, 0];
|
|
137
|
+
if (key === MISCELLANEOUS) return [2, 0];
|
|
192
138
|
if (labels) {
|
|
193
139
|
const configured = labels.indexOf(key);
|
|
194
140
|
return configured === -1 ? [1, appearanceOrder.indexOf(key)] : [0, configured];
|
|
@@ -197,26 +143,40 @@ function groupRank(key, appearanceOrder, labels) {
|
|
|
197
143
|
}
|
|
198
144
|
|
|
199
145
|
/**
|
|
200
|
-
*
|
|
146
|
+
* `changelog group`: give the newest release its final shape.
|
|
147
|
+
*
|
|
148
|
+
* Every earlier release section is untouched, byte for byte. Running this on
|
|
149
|
+
* its own output is a no-op.
|
|
201
150
|
*
|
|
202
|
-
* @param {string}
|
|
203
|
-
*
|
|
204
|
-
* @param {readonly string[]|null} labels - `
|
|
205
|
-
* order
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* first-occurring line within `bodyText` — empty when `labels` is `null`,
|
|
210
|
-
* since nothing is "unknown" against no vocabulary.
|
|
151
|
+
* @param {string} text - The changelog's full contents.
|
|
152
|
+
* @param {object} [opts]
|
|
153
|
+
* @param {readonly string[]|null} [opts.labels] - the changelog `labels`, in
|
|
154
|
+
* display order. `null`/absent orders by first appearance and reports
|
|
155
|
+
* nothing as undeclared.
|
|
156
|
+
* @returns {{text: string, findings: Array<{line: number,
|
|
157
|
+
* severity: "warning", message: string}>}}
|
|
211
158
|
*/
|
|
212
|
-
function
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
if (blocks.length === 0) return { text: "", unknown: [] };
|
|
159
|
+
export function groupChangelogText(text, { labels = null } = {}) {
|
|
160
|
+
const range = releaseSectionRange(text);
|
|
161
|
+
if (!range) return { text, findings: [] };
|
|
216
162
|
|
|
217
|
-
|
|
163
|
+
const section = text.slice(range.start, range.end);
|
|
164
|
+
const headingEnd = section.indexOf("\n");
|
|
165
|
+
if (headingEnd === -1) return { text, findings: [] };
|
|
166
|
+
const heading = section.slice(0, headingEnd);
|
|
167
|
+
|
|
168
|
+
// The scaffold headings become blank lines, so every line keeps its number.
|
|
169
|
+
const codeLines = codeLineSet(section.slice(headingEnd + 1));
|
|
170
|
+
const body = section
|
|
171
|
+
.slice(headingEnd + 1)
|
|
172
|
+
.split("\n")
|
|
173
|
+
.map((line, i) => (!codeLines.has(i + 1) && CHANGES_HEADING_RE.test(line) ? "" : line))
|
|
174
|
+
.join("\n");
|
|
175
|
+
|
|
176
|
+
const blocks = topLevelBlocks(body, codeLines);
|
|
177
|
+
/** @type {Array<string|null>} */
|
|
218
178
|
const appearanceOrder = [];
|
|
219
|
-
/** @type {Map<string|null, Array<{label: string|null, text: string}>>} */
|
|
179
|
+
/** @type {Map<string|null, Array<{startLine: number, label: string|null, text: string}>>} */
|
|
220
180
|
const groups = new Map();
|
|
221
181
|
for (const block of blocks) {
|
|
222
182
|
if (!groups.has(block.label)) {
|
|
@@ -225,77 +185,28 @@ function groupSection(bodyText, labels) {
|
|
|
225
185
|
}
|
|
226
186
|
groups.get(block.label).push(block);
|
|
227
187
|
}
|
|
228
|
-
|
|
229
188
|
const sortedKeys = [...appearanceOrder].sort((a, b) => {
|
|
230
189
|
const [aTier, aRank] = groupRank(a, appearanceOrder, labels);
|
|
231
190
|
const [bTier, bRank] = groupRank(b, appearanceOrder, labels);
|
|
232
191
|
return aTier - bTier || aRank - bRank;
|
|
233
192
|
});
|
|
234
193
|
|
|
235
|
-
const
|
|
236
|
-
labels === null ?
|
|
237
|
-
[]
|
|
238
|
-
: sortedKeys
|
|
239
|
-
.filter((key) => key !== null && !labels.includes(key))
|
|
240
|
-
.map((key) => ({ label: key, startLine: groups.get(key)[0].startLine }));
|
|
241
|
-
|
|
242
|
-
const text = sortedKeys.map((key) => mergeGroup(groups.get(key))).join("\n\n");
|
|
243
|
-
return { text, unknown };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* `changelog group`: fold the newest release's changeset blocks together by
|
|
248
|
-
* their bold label, order them, and file an undeclared one last.
|
|
249
|
-
*
|
|
250
|
-
* Every earlier release section is untouched, byte for byte — only the
|
|
251
|
-
* first `## <version>` section's `### <Bump> Changes` bodies are rewritten,
|
|
252
|
-
* each independently (a label groups within its own bump level, never
|
|
253
|
-
* across one). Running this on its own output is a no-op: a release already
|
|
254
|
-
* in label order, with each label merged to one block, groups to itself.
|
|
255
|
-
*
|
|
256
|
-
* @param {string} text - The changelog's full contents.
|
|
257
|
-
* @param {object} [opts]
|
|
258
|
-
* @param {readonly string[]|null} [opts.labels] - `changelog.labels`, in
|
|
259
|
-
* display order. `null`/absent orders every group by first appearance
|
|
260
|
-
* instead, lead paragraph first, and files nothing as unknown.
|
|
261
|
-
* @returns {{text: string, findings: Array<{line: number,
|
|
262
|
-
* severity: "warning", message: string}>}}
|
|
263
|
-
*/
|
|
264
|
-
export function groupChangelogText(text, { labels = null } = {}) {
|
|
265
|
-
const range = releaseSectionRange(text);
|
|
266
|
-
if (!range) return { text, findings: [] };
|
|
267
|
-
|
|
268
|
-
const section = text.slice(range.start, range.end);
|
|
269
|
-
const codeLines = codeLineSet(section);
|
|
270
|
-
const headings = [...section.matchAll(CHANGES_HEADING_RE)].filter(
|
|
271
|
-
(heading) => !codeLines.has(lineColOf(section, heading.index).line),
|
|
272
|
-
);
|
|
273
|
-
if (headings.length === 0) return { text, findings: [] };
|
|
274
|
-
|
|
275
|
-
let rebuilt = section.slice(0, headings[0].index);
|
|
276
|
-
/** @type {Array<{line: number, severity: "warning", message: string}>} */
|
|
194
|
+
const { line: bodyFirstLine } = lineColOf(text, range.start + headingEnd + 1);
|
|
277
195
|
const findings = [];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const bodyStart = heading.index + heading[0].length;
|
|
282
|
-
const bodyEnd = i + 1 < headings.length ? headings[i + 1].index : section.length;
|
|
283
|
-
const { text: grouped, unknown } = groupSection(section.slice(bodyStart, bodyEnd), labels);
|
|
284
|
-
|
|
285
|
-
const isVeryLast = i === headings.length - 1 && range.end === text.length;
|
|
286
|
-
rebuilt += heading[0] + (grouped ? `\n\n${grouped}` : "") + (isVeryLast ? "\n" : "\n\n");
|
|
287
|
-
|
|
288
|
-
const { line: bodyFirstLine } = lineColOf(text, range.start + bodyStart);
|
|
289
|
-
for (const { label, startLine } of unknown) {
|
|
196
|
+
if (labels)
|
|
197
|
+
for (const key of sortedKeys) {
|
|
198
|
+
if (key === null || key === MISCELLANEOUS || labels.includes(key)) continue;
|
|
290
199
|
findings.push({
|
|
291
|
-
line: bodyFirstLine + (startLine - 1),
|
|
292
|
-
severity: "warning",
|
|
200
|
+
line: bodyFirstLine + (groups.get(key)[0].startLine - 1),
|
|
201
|
+
severity: /** @type {const} */ ("warning"),
|
|
293
202
|
message:
|
|
294
|
-
`changelog-group/unknown-label "${
|
|
295
|
-
"`
|
|
203
|
+
`changelog-group/unknown-label "${key}" is not declared in ` +
|
|
204
|
+
"the changelog `labels` — filed after the declared labels, in order of first appearance",
|
|
296
205
|
});
|
|
297
206
|
}
|
|
298
|
-
}
|
|
299
207
|
|
|
208
|
+
const grouped = sortedKeys.map((key) => mergeGroup(groups.get(key))).join("\n\n");
|
|
209
|
+
const rebuilt =
|
|
210
|
+
heading + (grouped ? `\n\n${grouped}` : "") + (range.end === text.length ? "\n" : "\n\n");
|
|
300
211
|
return { text: text.slice(0, range.start) + rebuilt + text.slice(range.end), findings };
|
|
301
212
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The labels a repository's changesets group under.
|
|
16
|
+
*
|
|
17
|
+
* They are the options of the changelog entry in `.changeset/config.json`,
|
|
18
|
+
* beside the generator they configure:
|
|
19
|
+
*
|
|
20
|
+
* ```json
|
|
21
|
+
* "changelog": ["@heroiclands/package-build/changelog", { "labels": ["Verse", "Rendering"] }]
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* `changelog check` accepts a label only if it is listed there or is
|
|
25
|
+
* `Miscellaneous`, which every repository has and never declares.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
|
|
33
|
+
import { MISCELLANEOUS } from "./changelog-lint.mjs";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read `labels` from the changelog options in `.changeset/config.json`.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [cwd] - The repository root.
|
|
39
|
+
* @returns {readonly string[]|null} The labels in display order, or `null`
|
|
40
|
+
* when the file, the options or `labels` is absent.
|
|
41
|
+
* @throws {Error} When `config.json` is not JSON, or `labels` is not an array
|
|
42
|
+
* of distinct non-empty strings or names `Miscellaneous`.
|
|
43
|
+
*/
|
|
44
|
+
export function readChangelogLabels(cwd = process.cwd()) {
|
|
45
|
+
const file = path.join(cwd, ".changeset", "config.json");
|
|
46
|
+
if (!fs.existsSync(file)) return null;
|
|
47
|
+
const where = path.relative(cwd, file);
|
|
48
|
+
let config;
|
|
49
|
+
try {
|
|
50
|
+
config = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new Error(`${where}: is not valid JSON (${error.message})`);
|
|
53
|
+
}
|
|
54
|
+
const entry = config?.changelog;
|
|
55
|
+
const options = Array.isArray(entry) ? entry[1] : undefined;
|
|
56
|
+
if (options === undefined || options === null) return null;
|
|
57
|
+
if (typeof options !== "object" || Array.isArray(options))
|
|
58
|
+
throw new Error(`${where}: the changelog options must be an object`);
|
|
59
|
+
if (options.labels === undefined) return null;
|
|
60
|
+
const { labels } = options;
|
|
61
|
+
const bad = (problem) => new Error(`${where}: changelog \`labels\` ${problem}`);
|
|
62
|
+
if (!Array.isArray(labels)) throw bad("must be an array of strings");
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const label of labels) {
|
|
65
|
+
if (typeof label !== "string" || label.trim() === "")
|
|
66
|
+
throw bad("must hold only non-empty strings");
|
|
67
|
+
if (label === MISCELLANEOUS)
|
|
68
|
+
throw bad(`declares \`${MISCELLANEOUS}\`, which is always available and always last`);
|
|
69
|
+
if (seen.has(label)) throw bad(`declares \`${label}\` more than once`);
|
|
70
|
+
seen.add(label);
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze([...labels]);
|
|
73
|
+
}
|