@heroiclands/package-build 18.1.0 → 18.1.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/CHANGELOG.md +26 -0
- package/engine/frontmatter-lint.mjs +12 -1
- package/engine/homepage.mjs +16 -2
- package/package.json +1 -1
- package/types/engine/homepage.d.mts +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 18.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 029cf03: **A homepage note is no longer refused for an `id` it did not author** (#319).
|
|
8
|
+
|
|
9
|
+
`resolveNoteId` fills `fm.id` **in place**, so every downstream reader sees one
|
|
10
|
+
derived value — deliberate, and documented as such. The homepage refusal
|
|
11
|
+
iterated that same object, so a note that authors no `id` was reported with a
|
|
12
|
+
message telling the author to delete a field that is not in the file.
|
|
13
|
+
|
|
14
|
+
It was an **error**, so it failed `lint:addresses`, and `lint` heads the build
|
|
15
|
+
chain — which meant it failed every pull request opened against a repository
|
|
16
|
+
carrying a homepage note, whatever that pull request changed.
|
|
17
|
+
|
|
18
|
+
A refused field must now be one the note actually wrote. The caller already owns
|
|
19
|
+
the raw note text and already positions these findings with it, so it answers
|
|
20
|
+
which keys are declared at the note's own top level; `positionInFrontmatter`'s
|
|
21
|
+
`topLevel` option is the existing helper for exactly that question, so a nested
|
|
22
|
+
`id:` under some other key is not mistaken for the note's own. With no answer
|
|
23
|
+
supplied, every key in `fm` still counts — the previous behaviour, and the right
|
|
24
|
+
one for a caller holding authored frontmatter only.
|
|
25
|
+
|
|
26
|
+
Measured on `sohl`: `lint:addresses` goes from one error to **clean across all
|
|
27
|
+
1,685 notes**.
|
|
28
|
+
|
|
3
29
|
## 18.1.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
|
@@ -890,7 +890,18 @@ export function lintNote(
|
|
|
890
890
|
// so the finding must survive the early returns below.
|
|
891
891
|
findings.push(...checkTags(note, { type }));
|
|
892
892
|
|
|
893
|
-
|
|
893
|
+
// A refused field must be one the note *wrote*: `resolveNoteId` fills
|
|
894
|
+
// `fm.id` in place, so the parsed frontmatter carries a derived id the
|
|
895
|
+
// author never typed (#319). The raw text is the only place that
|
|
896
|
+
// distinguishes them, and `positionInFrontmatter` already answers it —
|
|
897
|
+
// `topLevel` so a nested `id:` under some other key is not mistaken for the
|
|
898
|
+
// note's own.
|
|
899
|
+
const authoredAtTopLevel = (key) =>
|
|
900
|
+
positionInFrontmatter(note.raw ?? "", key, undefined, { topLevel: true }).line !==
|
|
901
|
+
undefined;
|
|
902
|
+
for (const { locator, message } of checkHomepageAddressFields(fm, {
|
|
903
|
+
isAuthored: authoredAtTopLevel,
|
|
904
|
+
})) {
|
|
894
905
|
findings.push({
|
|
895
906
|
file: note.file,
|
|
896
907
|
...at(locator.key, locator.literal),
|
package/engine/homepage.mjs
CHANGED
|
@@ -192,12 +192,24 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
|
|
|
192
192
|
* belong to the caller. This mirrors {@link module:engine/retired-fields},
|
|
193
193
|
* whose retired-field messages are likewise positioned by whoever reports them.
|
|
194
194
|
*
|
|
195
|
+
* **A refused field must be one the note *wrote*.** `resolveNoteId` fills
|
|
196
|
+
* `fm.id` **in place** so every downstream reader sees one derived value —
|
|
197
|
+
* deliberately, and documented as such — and this ran over the same object, so
|
|
198
|
+
* a homepage that authors no `id` was told to delete one that is not there
|
|
199
|
+
* (#319). Since the caller already owns the raw note text, it also answers
|
|
200
|
+
* which keys the note actually declared; without an answer every key in `fm`
|
|
201
|
+
* counts, which is the old behaviour and right for a caller holding authored
|
|
202
|
+
* frontmatter only.
|
|
203
|
+
*
|
|
195
204
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
205
|
+
* @param {object} [options] - Options.
|
|
206
|
+
* @param {(key: string) => boolean} [options.isAuthored] - Whether the note
|
|
207
|
+
* declares this key at its own top level. Defaults to "every key in `fm`".
|
|
196
208
|
* @returns {Array<{field: string, locator: {key: string, literal?: string},
|
|
197
209
|
* message: string}>} One entry per finding, empty for any note that is not a
|
|
198
210
|
* homepage and declares nothing wrong.
|
|
199
211
|
*/
|
|
200
|
-
export function checkHomepageAddressFields(fm) {
|
|
212
|
+
export function checkHomepageAddressFields(fm, { isAuthored } = {}) {
|
|
201
213
|
if (!isHomepage(fm)) return [];
|
|
202
214
|
const out = [];
|
|
203
215
|
|
|
@@ -218,7 +230,9 @@ export function checkHomepageAddressFields(fm) {
|
|
|
218
230
|
|
|
219
231
|
for (const key of Object.keys(fm)) {
|
|
220
232
|
const message = HOMEPAGE_REFUSED_FIELDS.get(key);
|
|
221
|
-
if (message)
|
|
233
|
+
if (!message) continue;
|
|
234
|
+
if (isAuthored && !isAuthored(key)) continue;
|
|
235
|
+
out.push({ field: key, locator: { key }, message });
|
|
222
236
|
}
|
|
223
237
|
return out;
|
|
224
238
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "18.1.
|
|
3
|
+
"version": "18.1.1",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -42,12 +42,26 @@ export function isHomepage(fm: object | null | undefined): boolean;
|
|
|
42
42
|
* belong to the caller. This mirrors {@link module:engine/retired-fields},
|
|
43
43
|
* whose retired-field messages are likewise positioned by whoever reports them.
|
|
44
44
|
*
|
|
45
|
+
* **A refused field must be one the note *wrote*.** `resolveNoteId` fills
|
|
46
|
+
* `fm.id` **in place** so every downstream reader sees one derived value —
|
|
47
|
+
* deliberately, and documented as such — and this ran over the same object, so
|
|
48
|
+
* a homepage that authors no `id` was told to delete one that is not there
|
|
49
|
+
* (#319). Since the caller already owns the raw note text, it also answers
|
|
50
|
+
* which keys the note actually declared; without an answer every key in `fm`
|
|
51
|
+
* counts, which is the old behaviour and right for a caller holding authored
|
|
52
|
+
* frontmatter only.
|
|
53
|
+
*
|
|
45
54
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
55
|
+
* @param {object} [options] - Options.
|
|
56
|
+
* @param {(key: string) => boolean} [options.isAuthored] - Whether the note
|
|
57
|
+
* declares this key at its own top level. Defaults to "every key in `fm`".
|
|
46
58
|
* @returns {Array<{field: string, locator: {key: string, literal?: string},
|
|
47
59
|
* message: string}>} One entry per finding, empty for any note that is not a
|
|
48
60
|
* homepage and declares nothing wrong.
|
|
49
61
|
*/
|
|
50
|
-
export function checkHomepageAddressFields(fm: object | null | undefined
|
|
62
|
+
export function checkHomepageAddressFields(fm: object | null | undefined, { isAuthored }?: {
|
|
63
|
+
isAuthored?: ((key: string) => boolean) | undefined;
|
|
64
|
+
}): Array<{
|
|
51
65
|
field: string;
|
|
52
66
|
locator: {
|
|
53
67
|
key: string;
|