@heroiclands/package-build 3.3.0 → 4.0.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 +179 -0
- package/CONTENT.md +29 -12
- package/MIGRATING.md +47 -0
- package/engine/base-compiler.mjs +33 -36
- package/engine/content-links.mjs +15 -13
- package/engine/content-package.mjs +3 -2
- package/engine/field-reference.mjs +2 -2
- package/engine/frontmatter-lint.mjs +26 -0
- package/engine/helpers.mjs +10 -12
- package/engine/index.mjs +4 -1
- package/engine/journals.mjs +2 -3
- package/engine/macros.mjs +2 -3
- package/engine/manifest-emit.mjs +15 -11
- package/engine/note-package.mjs +75 -68
- package/engine/pack-router.mjs +3 -3
- package/engine/retired-fields.mjs +123 -0
- package/engine/scenes.mjs +7 -11
- package/engine/site-build.mjs +30 -11
- package/engine/site-index.mjs +5 -5
- package/package.json +1 -1
- package/sohl/actors.mjs +2 -3
- package/sohl/item-fields.mjs +0 -8
- package/sohl/items.mjs +2 -3
- package/types/engine/base-compiler.d.mts +12 -17
- package/types/engine/content-package.d.mts +3 -2
- package/types/engine/helpers.d.mts +5 -8
- package/types/engine/index.d.mts +1 -0
- package/types/engine/manifest-emit.d.mts +7 -8
- package/types/engine/note-package.d.mts +29 -34
- package/types/engine/pack-router.d.mts +3 -3
- package/types/engine/retired-fields.d.mts +54 -0
- package/types/engine/site-build.d.mts +10 -0
package/engine/journals.mjs
CHANGED
|
@@ -38,9 +38,8 @@
|
|
|
38
38
|
* Not a standalone script — exports the `Journals` compiler class, imported
|
|
39
39
|
* and driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
40
40
|
*
|
|
41
|
-
* The walk itself — filtering by
|
|
42
|
-
*
|
|
43
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
41
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
42
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
44
43
|
* states only what makes this pass its own (#1509).
|
|
45
44
|
*/
|
|
46
45
|
|
package/engine/macros.mjs
CHANGED
|
@@ -44,9 +44,8 @@
|
|
|
44
44
|
* Not a standalone script — exports the `Macros` compiler class, imported and
|
|
45
45
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
46
46
|
*
|
|
47
|
-
* The walk itself — filtering by
|
|
48
|
-
*
|
|
49
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
47
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
48
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
50
49
|
* states only what makes this pass its own (#1509).
|
|
51
50
|
*/
|
|
52
51
|
|
package/engine/manifest-emit.mjs
CHANGED
|
@@ -54,7 +54,8 @@ import { canonicalKey, writeManifests } from "./kb-manifest.mjs";
|
|
|
54
54
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
55
55
|
import { compendiumUuid, packForType, pageUuid } from "./ids.mjs";
|
|
56
56
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
57
|
-
import {
|
|
57
|
+
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
58
|
+
import { assertNoDraftField } from "./retired-fields.mjs";
|
|
58
59
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
59
60
|
import { routerFor } from "./pack-router.mjs";
|
|
60
61
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -193,14 +194,13 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
193
194
|
/**
|
|
194
195
|
* Every note this package publishes, as manifest entries.
|
|
195
196
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* filtered out of a manifest that then claimed the package published nothing.
|
|
197
|
+
* Every note in the tree is this package's, so nothing here selects by package:
|
|
198
|
+
* the key's first segment is `contentPackage` (#56). A note still declaring the
|
|
199
|
+
* retired `package:` or `draft:` field **throws** rather than being skipped —
|
|
200
|
+
* skipping one silently is how a whole tree came to be filtered out of a
|
|
201
|
+
* manifest that then claimed the package published nothing, and it is what let
|
|
202
|
+
* a drafted note's inbound links look like links to a note that never existed
|
|
203
|
+
* (#69).
|
|
204
204
|
*
|
|
205
205
|
* A note that has no address is **reported, not guessed** — the finding carries
|
|
206
206
|
* the file and the reason, so a caller can print it or fail on it. Inventing an
|
|
@@ -226,8 +226,12 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
226
226
|
)) {
|
|
227
227
|
if (!fm) continue;
|
|
228
228
|
const rel = path.relative(contentBase, absPath);
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
assertNoDeclaredPackage(fm, {
|
|
230
|
+
file: rel,
|
|
231
|
+
absPath,
|
|
232
|
+
configured: ctx.contentPackage,
|
|
233
|
+
});
|
|
234
|
+
assertNoDraftField(fm, { file: rel, absPath });
|
|
231
235
|
if (!fm.type || !fm.shortcode) continue;
|
|
232
236
|
|
|
233
237
|
const base = path.basename(absPath);
|
package/engine/note-package.mjs
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Which content package a note belongs to.
|
|
16
16
|
*
|
|
17
|
-
* **It is the repository's configured `contentPackage
|
|
18
|
-
* exactly one package's notes — every package is
|
|
19
|
-
* repository that ships it — so the package is a property
|
|
20
|
-
* not of the note.
|
|
17
|
+
* **It is the repository's configured `contentPackage`, and nothing else.** A
|
|
18
|
+
* content tree holds exactly one package's notes — every package is
|
|
19
|
+
* single-sourced in the repository that ships it — so the package is a property
|
|
20
|
+
* of the *repository*, not of the note, and no note declares it.
|
|
21
21
|
*
|
|
22
22
|
* It used to be a property of the note, and a **selector**: the compile loop
|
|
23
23
|
* read `package:` out of frontmatter and skipped anything that did not match
|
|
@@ -27,100 +27,107 @@
|
|
|
27
27
|
* **zero notes and exited 0** — which is exactly the state the un-migrated
|
|
28
28
|
* `hm-loc-*` / `hm-adv-*` repositories are in today.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
30
|
+
* The field was retired in three steps, and all three have landed:
|
|
31
31
|
*
|
|
32
|
-
* 1. **Optional
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 2. **Swept** out of every content tree
|
|
36
|
-
*
|
|
32
|
+
* 1. **Optional** (3.3.0). An absent `package:` was normal and a present one
|
|
33
|
+
* was accepted while it agreed. Non-breaking, so a consumer adopted it
|
|
34
|
+
* before changing a note.
|
|
35
|
+
* 2. **Swept** out of every content tree on the org — 6,235 notes across
|
|
36
|
+
* `sohl`, `thalorna`, `kethira` and `harnensemble`.
|
|
37
|
+
* 3. **Rejected outright**, as this major. A note declaring the field fails the
|
|
38
|
+
* build naming the file, whatever the value says.
|
|
37
39
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* Step 3 is deliberately a rejection rather than continued tolerance: a field
|
|
41
|
+
* accepted while it agrees is a field that grows back, one note at a time, and
|
|
42
|
+
* every one of them is a line restating a constant the configuration already
|
|
43
|
+
* carries. There is no value that makes writing it correct, so the diagnostic
|
|
44
|
+
* says what to write instead rather than which value to change.
|
|
45
|
+
*
|
|
46
|
+
* **One `package` survives, and it is synthesised.** A `dataview` table scopes
|
|
47
|
+
* itself with `WHERE … and package = "<pkg>"`, and that clause resolves against
|
|
48
|
+
* frontmatter like any other field. {@link searchableFrontmatter} supplies the
|
|
49
|
+
* derived value to the search so the 45 authored clauses across `sohl` and
|
|
50
|
+
* `thalorna` keep matching. It is a *search* value, never an authored one, and
|
|
51
|
+
* it is never written back to a note.
|
|
45
52
|
*
|
|
46
53
|
* @module
|
|
47
54
|
*/
|
|
48
55
|
|
|
49
56
|
import { contentPackage } from "./content-package.mjs";
|
|
57
|
+
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* Non-validating: the answer for a note that declares nothing, and for one that
|
|
55
|
-
* declares the configured package, is the same value. A note declaring some
|
|
56
|
-
* *other* package is answered literally here rather than corrected — the
|
|
57
|
-
* compile pass reports that, once, through {@link assertNotePackage}.
|
|
58
|
-
*
|
|
59
|
-
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
60
|
-
* could not be parsed.
|
|
61
|
-
* @param {string} [configured] - The package this build compiles. Defaults to
|
|
62
|
-
* the configured `contentPackage`; passed explicitly by callers that already
|
|
63
|
-
* carry it in a context object, so a caller's configuration drives every read.
|
|
64
|
-
* @returns {string} The package.
|
|
65
|
-
*/
|
|
66
|
-
export function notePackage(fm, configured) {
|
|
67
|
-
const declared = fm?.package;
|
|
68
|
-
// A blank is a declaration of nothing, not a package named "".
|
|
69
|
-
if (declared != null && declared !== "") return declared;
|
|
70
|
-
// Resolved only when it is needed, so a caller holding a note that declares
|
|
71
|
-
// one never touches the configuration (#2).
|
|
72
|
-
return configured ?? contentPackage();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* A note's frontmatter as a generated table searches it — its package present
|
|
77
|
-
* whether or not the note declares one.
|
|
60
|
+
* A note's frontmatter as a generated table searches it — its package present,
|
|
61
|
+
* though no note declares one.
|
|
78
62
|
*
|
|
79
63
|
* A `dataview` query resolves `package` out of frontmatter like any other
|
|
80
64
|
* field, so a collection note that scopes itself with `WHERE … and package =
|
|
81
|
-
* "sohl"`
|
|
82
|
-
* table** in silence.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* way.
|
|
65
|
+
* "sohl"` would match nothing now that the field is gone, and would render an
|
|
66
|
+
* **empty table** in silence. Supplying the derived value here is what kept the
|
|
67
|
+
* sweep mechanical rather than a trap (#56) — and a query that never mentions
|
|
68
|
+
* `package` is unaffected either way.
|
|
86
69
|
*
|
|
87
|
-
* The
|
|
88
|
-
*
|
|
70
|
+
* The frontmatter is copied rather than written into: it is the note's own
|
|
71
|
+
* parsed object, shared with every other reader, and the derived package is a
|
|
72
|
+
* property of *this search*, not of the note.
|
|
89
73
|
*
|
|
90
74
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
91
|
-
* @param {string} [configured] - The package this build compiles.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
75
|
+
* @param {string} [configured] - The package this build compiles. Defaults to
|
|
76
|
+
* the configured `contentPackage`; passed explicitly by callers that already
|
|
77
|
+
* carry it in a context object, so a caller's configuration drives every read.
|
|
78
|
+
* @returns {object|null|undefined} A shallow copy carrying the derived package,
|
|
79
|
+
* or whatever was passed when it is not frontmatter at all.
|
|
94
80
|
*/
|
|
95
81
|
export function searchableFrontmatter(fm, configured) {
|
|
96
82
|
if (!fm || typeof fm !== "object") return fm;
|
|
97
|
-
|
|
98
|
-
return { ...fm, package: notePackage(fm, configured) };
|
|
83
|
+
return { ...fm, package: configured ?? contentPackage() };
|
|
99
84
|
}
|
|
100
85
|
|
|
101
86
|
/**
|
|
102
|
-
*
|
|
87
|
+
* Refuse a note that declares `package:` at all.
|
|
103
88
|
*
|
|
104
|
-
*
|
|
89
|
+
* Presence is the whole test — a declaration that *agrees* with the
|
|
90
|
+
* configuration is as retired as one that disagrees, and an empty one
|
|
91
|
+
* (`package:`, which parses as `null`) is still the field.
|
|
92
|
+
*
|
|
93
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
94
|
+
* could not be parsed.
|
|
105
95
|
* @param {object} [options] - Options.
|
|
106
96
|
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
107
97
|
* it where the caller emits through a diagnostic, which puts the locator at
|
|
108
98
|
* the start of the line already — repeating it prints the path twice.
|
|
99
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
100
|
+
* failing path to locate the offending line and column. The position rides on
|
|
101
|
+
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
109
102
|
* @param {string} [options.configured] - The package this build compiles.
|
|
110
103
|
* Defaults to the configured `contentPackage`.
|
|
111
|
-
* @returns {
|
|
112
|
-
* @throws {Error} When the note declares
|
|
104
|
+
* @returns {void}
|
|
105
|
+
* @throws {Error} When the note declares the field.
|
|
113
106
|
*/
|
|
114
|
-
export function
|
|
107
|
+
export function assertNoDeclaredPackage(
|
|
108
|
+
fm,
|
|
109
|
+
{ file, absPath, configured } = {},
|
|
110
|
+
) {
|
|
111
|
+
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "package")) return;
|
|
112
|
+
|
|
115
113
|
const target = configured ?? contentPackage();
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
const declared = fm.package;
|
|
115
|
+
const wrote =
|
|
116
|
+
declared === null || declared === undefined || declared === "" ?
|
|
117
|
+
"`package:`"
|
|
118
|
+
: `\`package: ${declared}\``;
|
|
119
|
+
|
|
120
|
+
const err = new Error(
|
|
121
|
+
`${wrote} is a retired frontmatter field — delete it` +
|
|
121
122
|
(file ? ` — ${file}` : "") +
|
|
122
|
-
`. A note's package is
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
`. A note's package is this repository's configured ` +
|
|
124
|
+
`\`contentPackage\` ("${target}", in package-build.config.yaml), ` +
|
|
125
|
+
`and every note in the tree belongs to it.`,
|
|
125
126
|
);
|
|
127
|
+
// Where the field is, so the caller's diagnostic opens on the line that has
|
|
128
|
+
// to be deleted. Read here rather than carried through every walk: this is
|
|
129
|
+
// the failing path, and the build stops on it.
|
|
130
|
+
const position = locateFrontmatterKey(absPath, "package");
|
|
131
|
+
if (position) err.position = position;
|
|
132
|
+
throw err;
|
|
126
133
|
}
|
package/engine/pack-router.mjs
CHANGED
|
@@ -69,10 +69,10 @@ export class PackRoutingError extends Error {
|
|
|
69
69
|
/**
|
|
70
70
|
* The frontmatter field a note declares its pack in.
|
|
71
71
|
*
|
|
72
|
-
* Deliberately close to the
|
|
72
|
+
* Deliberately close to the retired `package:` and deliberately not the same
|
|
73
73
|
* word: `package:` said which *distribution* owned a note — now the
|
|
74
|
-
* repository's `contentPackage
|
|
75
|
-
* receives its document.
|
|
74
|
+
* repository's `contentPackage`, and no longer authorable (#56) — while `pack:`
|
|
75
|
+
* says which *compendium* receives its document.
|
|
76
76
|
*/
|
|
77
77
|
export const PACK_FIELD = "pack";
|
|
78
78
|
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
* Frontmatter fields a note may no longer declare.
|
|
16
|
+
*
|
|
17
|
+
* A retired field has exactly two possible fates, and only one of them is
|
|
18
|
+
* honest. Left honoured, it keeps doing whatever it did — which is why it was
|
|
19
|
+
* retired. Left *ignored*, it reads to its author as though it still works: the
|
|
20
|
+
* note says one thing and the build does another, and nothing says so. So a
|
|
21
|
+
* retired field is **refused**, naming the file and the line, and the message
|
|
22
|
+
* says what to write instead rather than which value to correct.
|
|
23
|
+
*
|
|
24
|
+
* `package:` is retired the same way and is refused from `note-package.mjs`,
|
|
25
|
+
* where the concept it belonged to still lives. `draft:` has no such home —
|
|
26
|
+
* there is no surviving concept it was part of — so it is refused here.
|
|
27
|
+
*
|
|
28
|
+
* **What `draft:` did (#69).** It excluded a note from the compiled packs, from
|
|
29
|
+
* the link manifest and from a consuming site build. Nothing reported the
|
|
30
|
+
* consequence: `content-links.mjs`, `site-index.mjs` and `content-lint.mjs`
|
|
31
|
+
* never read the field, so a link into a drafted note was indistinguishable
|
|
32
|
+
* from a link to a note that does not exist, and the checkers could not say
|
|
33
|
+
* which. The field's entire effect was to move a note from *published* to
|
|
34
|
+
* *unresolvable*, silently — and it also suppressed real build failures, since
|
|
35
|
+
* a note the compilers never reached could not fail on the defects it carried.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import fs from "node:fs";
|
|
41
|
+
|
|
42
|
+
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a note declaring `draft:` is told, in one place.
|
|
46
|
+
*
|
|
47
|
+
* Written once and shared by the compile-time refusal and the frontmatter lint,
|
|
48
|
+
* because an author meets whichever of the two runs first and they should read
|
|
49
|
+
* the same. It says what the field did and what to write instead, rather than
|
|
50
|
+
* which value to correct: no value makes declaring it right.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
53
|
+
* the caller emits through a diagnostic, whose locator already starts the
|
|
54
|
+
* line — repeating it prints the path twice.
|
|
55
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
56
|
+
*/
|
|
57
|
+
export function draftRetiredMessage(file) {
|
|
58
|
+
return (
|
|
59
|
+
"`draft:` is a retired frontmatter field — delete it" +
|
|
60
|
+
(file ? ` — ${file}` : "") +
|
|
61
|
+
". It excluded the note from the compiled packs, the link manifest " +
|
|
62
|
+
"and the site, and no checker reported the exclusion, so every " +
|
|
63
|
+
"wikilink into it read as a link to a note that does not exist. To " +
|
|
64
|
+
"mark a note as unfinished, tag it `#draft` instead: the build " +
|
|
65
|
+
"ignores tags, and a `FROM #draft` query still finds it"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Refuse a note that declares `draft:` at all.
|
|
71
|
+
*
|
|
72
|
+
* Presence is the whole test. `draft: false` is as retired as `draft: true` —
|
|
73
|
+
* it reads as "publish this note", which is what happens either way, and is
|
|
74
|
+
* exactly the belief the message exists to correct.
|
|
75
|
+
*
|
|
76
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
77
|
+
* could not be parsed.
|
|
78
|
+
* @param {object} [options] - Options.
|
|
79
|
+
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
80
|
+
* it where the caller emits through a diagnostic, which puts the locator at
|
|
81
|
+
* the start of the line already — repeating it prints the path twice.
|
|
82
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
83
|
+
* failing path to locate the offending line and column. The position rides on
|
|
84
|
+
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
85
|
+
* @returns {void}
|
|
86
|
+
* @throws {Error} When the note declares the field.
|
|
87
|
+
*/
|
|
88
|
+
export function assertNoDraftField(fm, { file, absPath } = {}) {
|
|
89
|
+
if (!fm || typeof fm !== "object" || !Object.hasOwn(fm, "draft")) return;
|
|
90
|
+
|
|
91
|
+
const err = new Error(`${draftRetiredMessage(file)}.`);
|
|
92
|
+
// Where the field is, so the caller's diagnostic opens on the line that has
|
|
93
|
+
// to be deleted. Read here rather than carried through every walk: this is
|
|
94
|
+
// the failing path, and the build stops on it.
|
|
95
|
+
const position = locateFrontmatterKey(absPath, "draft");
|
|
96
|
+
if (position) err.position = position;
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A frontmatter key's position in a note's file, or nothing.
|
|
102
|
+
*
|
|
103
|
+
* {@link positionInFrontmatter} answers the same question from the file's
|
|
104
|
+
* *text*; this reads the file to ask it. Kept apart from either caller because
|
|
105
|
+
* both refusals need it and a second copy is a second thing to keep correct.
|
|
106
|
+
*
|
|
107
|
+
* @param {string|undefined} absPath - The note's file.
|
|
108
|
+
* @param {string} key - The top-level frontmatter key.
|
|
109
|
+
* @returns {{line?: number, column?: number}|undefined} Spreadable position
|
|
110
|
+
* fields, dropped rather than guessed when the file cannot be read or the key
|
|
111
|
+
* cannot be found — as `formatDiagnostic` requires.
|
|
112
|
+
*/
|
|
113
|
+
export function locateFrontmatterKey(absPath, key) {
|
|
114
|
+
if (!absPath) return undefined;
|
|
115
|
+
let raw;
|
|
116
|
+
try {
|
|
117
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const at = positionInFrontmatter(raw, key);
|
|
122
|
+
return at.line === undefined ? undefined : at;
|
|
123
|
+
}
|
package/engine/scenes.mjs
CHANGED
|
@@ -42,9 +42,8 @@
|
|
|
42
42
|
* Not a standalone script — exports the `Scenes` compiler class, imported and
|
|
43
43
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
44
44
|
*
|
|
45
|
-
* The walk itself — filtering by
|
|
46
|
-
*
|
|
47
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
45
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
46
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
48
47
|
* states only what makes this pass its own (#1509).
|
|
49
48
|
*/
|
|
50
49
|
|
|
@@ -192,10 +191,11 @@ export class Scenes extends BasePackCompiler {
|
|
|
192
191
|
for (const { frontmatter: fm, body, absPath } of walkMarkdownTree(
|
|
193
192
|
this.contentBase,
|
|
194
193
|
)) {
|
|
195
|
-
// No
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
194
|
+
// No retired-field test: this pass's own walk — the shared compile
|
|
195
|
+
// loop — is where a note still declaring `package:` (#56) or
|
|
196
|
+
// `draft:` (#69) is reported, once. Repeating either check here
|
|
197
|
+
// would double the diagnostic or throw past it. A refused note is
|
|
198
|
+
// indexed and then never compiled, so it reaches no document.
|
|
199
199
|
if (!fm || !fm.id) continue;
|
|
200
200
|
if (
|
|
201
201
|
fm.shortcode &&
|
|
@@ -216,10 +216,6 @@ export class Scenes extends BasePackCompiler {
|
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
if (!isMapType(fm.type)) continue;
|
|
219
|
-
if (fm.draft === true) {
|
|
220
|
-
log.debug(`Skipping draft map: ${absPath}`);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
219
|
maps.push({ fm, body, absPath });
|
|
224
220
|
}
|
|
225
221
|
return { maps, effectsByAddress };
|
package/engine/site-build.mjs
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
} from "./foreign-manifests.mjs";
|
|
63
63
|
import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
64
64
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
65
|
-
import {
|
|
65
|
+
import { searchableFrontmatter } from "./note-package.mjs";
|
|
66
66
|
|
|
67
67
|
const require = createRequire(import.meta.url);
|
|
68
68
|
|
|
@@ -132,11 +132,10 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
132
132
|
const note = readNote(file);
|
|
133
133
|
if (!note) continue;
|
|
134
134
|
const { fm, body } = note;
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// (#56).
|
|
138
|
-
|
|
139
|
-
const pkg = notePackage(fm, ctx.contentPackage);
|
|
135
|
+
// The configuration's, never a note's: `package:` is retired, so every
|
|
136
|
+
// note in the tree belongs to the package this repository compiles
|
|
137
|
+
// (#56).
|
|
138
|
+
const pkg = ctx.contentPackage;
|
|
140
139
|
if (!ctx.packages.has(pkg) || !fm.type) continue;
|
|
141
140
|
|
|
142
141
|
for (const hit of frontmatterWikilinks(fm)) {
|
|
@@ -162,9 +161,9 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
162
161
|
pages.push({
|
|
163
162
|
kind: "content",
|
|
164
163
|
fm,
|
|
165
|
-
// The page's package,
|
|
164
|
+
// The page's package, recorded once here so every consumer — the
|
|
166
165
|
// index's canonical keys, the table universe, the local-package set
|
|
167
|
-
// — reads one
|
|
166
|
+
// — reads one configured value and never frontmatter (#56).
|
|
168
167
|
pkg,
|
|
169
168
|
body,
|
|
170
169
|
name,
|
|
@@ -347,7 +346,8 @@ export function tableUniverse(pages) {
|
|
|
347
346
|
const pkg = p.pkg;
|
|
348
347
|
if (!byPackage.has(pkg)) byPackage.set(pkg, []);
|
|
349
348
|
byPackage.get(pkg).push({
|
|
350
|
-
// Package present
|
|
349
|
+
// Package present for a `WHERE … package = "…"` clause,
|
|
350
|
+
// synthesised rather than authored — see
|
|
351
351
|
// {@link searchableFrontmatter} (#56).
|
|
352
352
|
fm: searchableFrontmatter(p.fm, pkg),
|
|
353
353
|
path: p.relPath,
|
|
@@ -367,6 +367,16 @@ export function tableUniverse(pages) {
|
|
|
367
367
|
* redirect stub at each name. They are dropped, and this build emits no
|
|
368
368
|
* redirects of its own.
|
|
369
369
|
*
|
|
370
|
+
* A content page carries the package the build **derived** (#65). No note
|
|
371
|
+
* declares one — `package:` is retired (#56) — so the note's frontmatter alone
|
|
372
|
+
* would publish a page that does not say which package it belongs to. The
|
|
373
|
+
* emitted page is what a
|
|
374
|
+
* theme reads: `breadcrumbs.html` builds its middle crumb from
|
|
375
|
+
* `.Params.package`, so without it that crumb degrades from a linked, labelled
|
|
376
|
+
* section to a bare type slug. Writing the derived value keeps a page
|
|
377
|
+
* self-describing and makes sweeping the field out of a content tree
|
|
378
|
+
* output-preserving for a site as it already is for the packs.
|
|
379
|
+
*
|
|
370
380
|
* @param {object} page - The page.
|
|
371
381
|
* @param {object} options - `{ sections, readmeSections, decorate }`.
|
|
372
382
|
* @returns {object} The frontmatter to write.
|
|
@@ -375,7 +385,15 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
|
375
385
|
const { fm, name, slug, sec, isReadme } = page;
|
|
376
386
|
let data;
|
|
377
387
|
if (page.kind === "content") {
|
|
378
|
-
data = {
|
|
388
|
+
data = {
|
|
389
|
+
...fm,
|
|
390
|
+
// Spread after the note's own frontmatter. Guarded because
|
|
391
|
+
// `package: undefined` is not a value YAML can carry.
|
|
392
|
+
...(page.pkg ? { package: page.pkg } : {}),
|
|
393
|
+
slug,
|
|
394
|
+
title: fm.title ?? name,
|
|
395
|
+
kbfolder: page.folder,
|
|
396
|
+
};
|
|
379
397
|
if (decorate) decorate(data, page);
|
|
380
398
|
if (isReadme) {
|
|
381
399
|
const meta = readmeSections[sec];
|
|
@@ -714,7 +732,8 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
714
732
|
|
|
715
733
|
const ctx = {
|
|
716
734
|
packages,
|
|
717
|
-
//
|
|
735
|
+
// The package every note in the tree belongs to. `package:` is
|
|
736
|
+
// retired, so this is the only source of it (#56).
|
|
718
737
|
contentPackage: resolved.contentPackage,
|
|
719
738
|
skipDirectories: resolved.skipDirectories,
|
|
720
739
|
mount,
|
package/engine/site-index.mjs
CHANGED
|
@@ -53,7 +53,7 @@ import path from "node:path";
|
|
|
53
53
|
|
|
54
54
|
import { canonicalKey, readCanonicalKey } from "./kb-manifest.mjs";
|
|
55
55
|
import { hasDocEntry } from "./item-docs.mjs";
|
|
56
|
-
import {
|
|
56
|
+
import { contentPackage } from "./content-package.mjs";
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* One page the site will publish, as the index needs to see it.
|
|
@@ -239,11 +239,11 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
239
239
|
// stays because a bare `[[skill-lang]]` defaults to the citing
|
|
240
240
|
// note's own package and must keep resolving unchanged; the
|
|
241
241
|
// canonical form is what cross-package links use (#1499).
|
|
242
|
-
// The page's package is
|
|
243
|
-
// and records it as `pkg
|
|
244
|
-
//
|
|
242
|
+
// The page's package is the configured one — the site collection
|
|
243
|
+
// resolves it and records it as `pkg`. Never read out of
|
|
244
|
+
// frontmatter: `package:` is retired (#56).
|
|
245
245
|
index.set(
|
|
246
|
-
canonicalKey(e.pkg ??
|
|
246
|
+
canonicalKey(e.pkg ?? contentPackage(), type, shortcode),
|
|
247
247
|
value,
|
|
248
248
|
);
|
|
249
249
|
// In Foundry an item and its documentation are two documents, so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
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",
|
package/sohl/actors.mjs
CHANGED
|
@@ -34,9 +34,8 @@
|
|
|
34
34
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`). Must run
|
|
35
35
|
* after the items pass, since it reads the items pack's generated JSON tree.
|
|
36
36
|
*
|
|
37
|
-
* The walk itself — filtering by
|
|
38
|
-
*
|
|
39
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
37
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
38
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
40
39
|
* states only what makes this pass its own (#1509).
|
|
41
40
|
*/
|
|
42
41
|
|
package/sohl/item-fields.mjs
CHANGED
|
@@ -598,14 +598,6 @@ export const ITEM_FIELDS = Object.freeze({
|
|
|
598
598
|
describe:
|
|
599
599
|
"Shortcode of the affiliation whose standing confers the ability — a religion, school, or ancestor/totem/spirit.",
|
|
600
600
|
},
|
|
601
|
-
{
|
|
602
|
-
name: "assocMysteryCode",
|
|
603
|
-
to: "assocMysteryCode",
|
|
604
|
-
ref: "mystery",
|
|
605
|
-
...AS_AUTHORED,
|
|
606
|
-
default: "",
|
|
607
|
-
describe: "Shortcode of the mystery the ability draws on.",
|
|
608
|
-
},
|
|
609
601
|
{
|
|
610
602
|
name: "masteryLevelBase",
|
|
611
603
|
to: "masteryLevelBase",
|
package/sohl/items.mjs
CHANGED
|
@@ -29,9 +29,8 @@
|
|
|
29
29
|
* Not a standalone script — exports the `Items` compiler class, imported and
|
|
30
30
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
31
31
|
*
|
|
32
|
-
* The walk itself — filtering by
|
|
33
|
-
*
|
|
34
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
32
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
33
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
35
34
|
* states only what makes this pass its own (#1509).
|
|
36
35
|
*/
|
|
37
36
|
|