@heroiclands/package-build 4.0.0 → 6.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 +401 -0
- package/CONTENT.md +207 -23
- package/MIGRATING.md +114 -0
- package/README.md +30 -0
- package/bin/content-build.mjs +26 -5
- package/bin/package-build.mjs +3 -1
- package/content-config.mjs +109 -6
- package/engine/base-compiler.mjs +28 -3
- package/engine/content-links.mjs +221 -2
- package/engine/content-lint.mjs +12 -3
- package/engine/diagnostics.mjs +46 -0
- package/engine/generate.mjs +156 -7
- package/engine/homepage.mjs +259 -0
- package/engine/index.mjs +6 -0
- package/engine/manifest-emit.mjs +2 -1
- package/engine/note-schemas.mjs +44 -0
- package/engine/pack-config.mjs +21 -0
- package/engine/site-build.mjs +140 -4
- package/manifest.mjs +195 -0
- package/package.json +1 -1
- package/sohl/actors.mjs +14 -1
- package/sohl/item-fields.mjs +0 -5
- package/sohl/kb-passes.mjs +81 -14
- package/sohl/note-schemas.mjs +8 -0
- package/types/content-config.d.mts +105 -4
- package/types/engine/base-compiler.d.mts +22 -0
- package/types/engine/content-links.d.mts +53 -2
- package/types/engine/diagnostics.d.mts +28 -0
- package/types/engine/generate.d.mts +50 -2
- package/types/engine/homepage.d.mts +125 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/note-schemas.d.mts +6 -0
- package/types/engine/pack-config.d.mts +13 -0
- package/types/engine/site-build.d.mts +54 -0
- package/types/manifest.d.mts +67 -1
- package/types/sohl/kb-passes.d.mts +5 -1
- package/types/sohl/note-schemas.d.mts +6 -0
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this package publishes the pages its content tree compiles to.
|
|
3
|
+
*
|
|
4
|
+
* The one question every reader of the mode actually asks — the site build, to
|
|
5
|
+
* decide whether to walk the tree at all, and the link-manifest emitter, to
|
|
6
|
+
* decide whether an entry carries a web `path`. Written once here so the two
|
|
7
|
+
* cannot come to disagree about what a mode means.
|
|
8
|
+
*
|
|
9
|
+
* @param {{publish: {site: SiteMode}}} config - A resolved configuration.
|
|
10
|
+
* @returns {boolean} Whether content pages are published.
|
|
11
|
+
*/
|
|
12
|
+
export function publishesContentPages(config: {
|
|
13
|
+
publish: {
|
|
14
|
+
site: SiteMode;
|
|
15
|
+
};
|
|
16
|
+
}): boolean;
|
|
1
17
|
/**
|
|
2
18
|
* Validate and normalize a content configuration.
|
|
3
19
|
*
|
|
@@ -76,6 +92,77 @@ export const DEFAULT_ADDRESS_SCHEME: Readonly<{
|
|
|
76
92
|
prefix: "";
|
|
77
93
|
landing: "readme";
|
|
78
94
|
}>;
|
|
95
|
+
/**
|
|
96
|
+
* How much of a package reaches the web.
|
|
97
|
+
*
|
|
98
|
+
* Every HeroicLands package publishes something: a top-level, human-authored
|
|
99
|
+
* homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
|
|
100
|
+
* module is, which system it needs and how to install it (#50). So there is no
|
|
101
|
+
* value here meaning *no web presence at all* — homepage-only is the **floor**,
|
|
102
|
+
* and the default.
|
|
103
|
+
*
|
|
104
|
+
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
105
|
+
* is not walked for pages, `site.sections` / `site.trees` / `site.landing`
|
|
106
|
+
* emit nothing, and link-manifest entries carry no web `path`.
|
|
107
|
+
* - `content` — the homepage *plus* every page the content tree publishes: the
|
|
108
|
+
* knowledgebase, the extra trees, the section landings.
|
|
109
|
+
*
|
|
110
|
+
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
111
|
+
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
112
|
+
* Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
|
|
113
|
+
* terms) must each publish a homepage and nothing beneath it — two packages
|
|
114
|
+
* under two different fan-content licences. The boundary is **published
|
|
115
|
+
* content**: journal text, artwork, item descriptions, compiled notes. A
|
|
116
|
+
* human-authored page announcing the module discloses none of it. Because the
|
|
117
|
+
* failure mode is silent — a `site:` block added later ships licensed content
|
|
118
|
+
* with nobody noticing — the mode fences the content surfaces off rather than
|
|
119
|
+
* trusting a configuration to stay empty.
|
|
120
|
+
*
|
|
121
|
+
* This was a boolean until 5.0.0, and `false` read as "no web presence", which
|
|
122
|
+
* no longer describes any package. Both spellings are refused rather than
|
|
123
|
+
* mapped: a value silently reinterpreted reads to its author as though it still
|
|
124
|
+
* means what it said.
|
|
125
|
+
*
|
|
126
|
+
* @typedef {"homepage" | "content"} SiteMode
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* The publishing modes {@link PublishSwitches.site} may name, floor first.
|
|
130
|
+
*
|
|
131
|
+
* @satisfies {readonly SiteMode[]}
|
|
132
|
+
*/
|
|
133
|
+
export const SITE_MODES: readonly ["homepage", "content"];
|
|
134
|
+
/**
|
|
135
|
+
* How much of a package reaches the web.
|
|
136
|
+
*
|
|
137
|
+
* Every HeroicLands package publishes something: a top-level, human-authored
|
|
138
|
+
* homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
|
|
139
|
+
* module is, which system it needs and how to install it (#50). So there is no
|
|
140
|
+
* value here meaning *no web presence at all* — homepage-only is the **floor**,
|
|
141
|
+
* and the default.
|
|
142
|
+
*
|
|
143
|
+
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
144
|
+
* is not walked for pages, `site.sections` / `site.trees` / `site.landing`
|
|
145
|
+
* emit nothing, and link-manifest entries carry no web `path`.
|
|
146
|
+
* - `content` — the homepage *plus* every page the content tree publishes: the
|
|
147
|
+
* knowledgebase, the extra trees, the section landings.
|
|
148
|
+
*
|
|
149
|
+
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
150
|
+
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
151
|
+
* Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
|
|
152
|
+
* terms) must each publish a homepage and nothing beneath it — two packages
|
|
153
|
+
* under two different fan-content licences. The boundary is **published
|
|
154
|
+
* content**: journal text, artwork, item descriptions, compiled notes. A
|
|
155
|
+
* human-authored page announcing the module discloses none of it. Because the
|
|
156
|
+
* failure mode is silent — a `site:` block added later ships licensed content
|
|
157
|
+
* with nobody noticing — the mode fences the content surfaces off rather than
|
|
158
|
+
* trusting a configuration to stay empty.
|
|
159
|
+
*
|
|
160
|
+
* This was a boolean until 5.0.0, and `false` read as "no web presence", which
|
|
161
|
+
* no longer describes any package. Both spellings are refused rather than
|
|
162
|
+
* mapped: a value silently reinterpreted reads to its author as though it still
|
|
163
|
+
* means what it said.
|
|
164
|
+
*/
|
|
165
|
+
export type SiteMode = "homepage" | "content";
|
|
79
166
|
export type PackageKind = "systems" | "modules";
|
|
80
167
|
export type PackDocumentType = "Actor" | "Adventure" | "Item" | "JournalEntry" | "Macro" | "Scene";
|
|
81
168
|
/**
|
|
@@ -272,9 +359,10 @@ export type ManifestSwitches = {
|
|
|
272
359
|
};
|
|
273
360
|
export type PublishSwitches = {
|
|
274
361
|
/**
|
|
275
|
-
*
|
|
362
|
+
* How much of this package reaches the web.
|
|
363
|
+
* See {@link SITE_MODES}.
|
|
276
364
|
*/
|
|
277
|
-
site:
|
|
365
|
+
site: SiteMode;
|
|
278
366
|
manifests: ManifestSwitches;
|
|
279
367
|
};
|
|
280
368
|
export type ManifestSwitchesInput = {
|
|
@@ -394,8 +482,19 @@ export type DocsSpec = {
|
|
|
394
482
|
itemFields?: DocPageSpec | undefined;
|
|
395
483
|
};
|
|
396
484
|
export type PublishSwitchesInput = {
|
|
397
|
-
site?:
|
|
485
|
+
site?: SiteMode | undefined;
|
|
398
486
|
manifests?: ManifestSwitchesInput | undefined;
|
|
487
|
+
address?: AddressSchemeInput | undefined;
|
|
488
|
+
};
|
|
489
|
+
export type AddressSchemeInput = {
|
|
490
|
+
/**
|
|
491
|
+
* Where the content tree mounts inside the package.
|
|
492
|
+
*/
|
|
493
|
+
prefix?: string | undefined;
|
|
494
|
+
/**
|
|
495
|
+
* Which note addresses a whole section.
|
|
496
|
+
*/
|
|
497
|
+
landing?: string | undefined;
|
|
399
498
|
};
|
|
400
499
|
/**
|
|
401
500
|
* One entry of a consumer's `itemBuilders` registry.
|
|
@@ -502,7 +601,9 @@ export type ContentBuildConfigInput = {
|
|
|
502
601
|
*/
|
|
503
602
|
relationships?: Relationships | undefined;
|
|
504
603
|
/**
|
|
505
|
-
* Publishing switches.
|
|
604
|
+
* Publishing switches. The manifest
|
|
605
|
+
* switches default to off; `site`
|
|
606
|
+
* defaults to `homepage`, the floor.
|
|
506
607
|
*/
|
|
507
608
|
publish?: PublishSwitchesInput | undefined;
|
|
508
609
|
};
|
|
@@ -57,6 +57,28 @@ export class BasePackCompiler {
|
|
|
57
57
|
* @type {boolean}
|
|
58
58
|
*/
|
|
59
59
|
static convertsWikilinks: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The document types whose **compiled output** this pass reads.
|
|
62
|
+
*
|
|
63
|
+
* Empty for every pass that reads only the content tree. The actors pass
|
|
64
|
+
* is the exception: a being names its embedded items by
|
|
65
|
+
* `(type, shortcode)`, and it resolves them against the JSON the item
|
|
66
|
+
* passes wrote — so an Actor pass must run after every Item pass, and it
|
|
67
|
+
* says so here.
|
|
68
|
+
*
|
|
69
|
+
* The generator derives the compile order from this (#73), so the order
|
|
70
|
+
* `packs:` declares is presentation only — it is the manifest's `packs`
|
|
71
|
+
* array as well, and a consumer orders that for a reader. A pass that
|
|
72
|
+
* reads another's output states the dependency once, in the class that
|
|
73
|
+
* does the reading, instead of every consuming repository having to know
|
|
74
|
+
* it when writing its pack list.
|
|
75
|
+
*
|
|
76
|
+
* A consumer registering a compiler of its own declares its dependencies
|
|
77
|
+
* the same way; a type no pack declares is simply not waited for.
|
|
78
|
+
*
|
|
79
|
+
* @type {readonly string[]}
|
|
80
|
+
*/
|
|
81
|
+
static readsPackOutputOf: readonly string[];
|
|
60
82
|
/**
|
|
61
83
|
* @param {object} options
|
|
62
84
|
* @param {string} options.contentBase - Root of the content tree.
|
|
@@ -26,18 +26,69 @@ export function buildLinkIndex(contentBase: string, { manifestDir, skipDirectori
|
|
|
26
26
|
manifestDir?: string | undefined;
|
|
27
27
|
skipDirectories?: readonly string[] | undefined;
|
|
28
28
|
}): object;
|
|
29
|
+
/**
|
|
30
|
+
* Every defect in the addresses a package homepage carries.
|
|
31
|
+
*
|
|
32
|
+
* **Why the homepage needs its own audit at all.** Every other note addresses
|
|
33
|
+
* the corpus with wikilinks, which {@link auditLinks} resolves. A homepage does
|
|
34
|
+
* not and cannot: it is published *verbatim* by every publishing mode, including
|
|
35
|
+
* the homepage-only mode two fan-licensed packages ship under, where the content
|
|
36
|
+
* tree is never walked and there is no index for a wikilink to resolve against.
|
|
37
|
+
* So a landing addresses the web the way the web does — markdown links and
|
|
38
|
+
* `url:` fields — and nothing was looking at those. SoHL's landing pointed at
|
|
39
|
+
* `kb/creature/` and `kb/character/` from the day those types merged into
|
|
40
|
+
* `being`: two 404s on the package's front page, through every build.
|
|
41
|
+
*
|
|
42
|
+
* **What is checkable, stated plainly.** Only an address into this site is, and
|
|
43
|
+
* only against facts this build already holds:
|
|
44
|
+
*
|
|
45
|
+
* - A **retired content type** in the path. The engine knows what used to exist
|
|
46
|
+
* and what replaced it, so this is a fact rather than a guess — and it is
|
|
47
|
+
* exactly the SoHL defect.
|
|
48
|
+
* - A **hardcoded absolute URL** into this package's own prefix, or into one a
|
|
49
|
+
* vendored manifest names. Both have a better form to write, which is why they
|
|
50
|
+
* are reported; a bare `/<package>/` is left alone, because a package
|
|
51
|
+
* homepage is in no manifest and there is nothing better to write.
|
|
52
|
+
* - A **root-relative `url:`**, which the theme's `relURL` prefixes a second
|
|
53
|
+
* time. `href:` means "already resolved, use verbatim", so the same leading
|
|
54
|
+
* slash is correct there and is not reported.
|
|
55
|
+
* - A **wikilink**, which nothing on this page will ever resolve.
|
|
56
|
+
*
|
|
57
|
+
* **What is not checkable, and is not attempted.** Whether an external URL
|
|
58
|
+
* answers — there is no network at build time, and a build must not fail because
|
|
59
|
+
* a third party is down. And whether a live in-site address names a page that
|
|
60
|
+
* exists: several of the surfaces a landing routes to are produced by other
|
|
61
|
+
* tools entirely (generated API documentation, hand-authored Hugo sections), so
|
|
62
|
+
* this build does not hold the set of published pages and would report a working
|
|
63
|
+
* link as dead.
|
|
64
|
+
*
|
|
65
|
+
* @param {ReturnType<typeof buildLinkIndex>} index - The built index.
|
|
66
|
+
* @returns {Array<{note: object, field: string, url: string, text: string,
|
|
67
|
+
* occurrence: number, message: string}>} One finding per defect, `text` and
|
|
68
|
+
* `occurrence` locating it in the note's raw source.
|
|
69
|
+
*/
|
|
70
|
+
export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Array<{
|
|
71
|
+
note: object;
|
|
72
|
+
field: string;
|
|
73
|
+
url: string;
|
|
74
|
+
text: string;
|
|
75
|
+
occurrence: number;
|
|
76
|
+
message: string;
|
|
77
|
+
}>;
|
|
29
78
|
/**
|
|
30
79
|
* Every link in a tree that lands nowhere.
|
|
31
80
|
*
|
|
32
81
|
* @param {ReturnType<typeof buildLinkIndex>} index - The built index.
|
|
33
82
|
* @returns {{deadAnchors: object[], deadAddresses: object[],
|
|
34
|
-
* frontmatterLinks: object[],
|
|
35
|
-
* which addresses a foreign
|
|
83
|
+
* frontmatterLinks: object[], homepageLinks: object[],
|
|
84
|
+
* usedManifest: Set<string>}} The findings, and which addresses a foreign
|
|
85
|
+
* manifest answered.
|
|
36
86
|
*/
|
|
37
87
|
export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
|
|
38
88
|
deadAnchors: object[];
|
|
39
89
|
deadAddresses: object[];
|
|
40
90
|
frontmatterLinks: object[];
|
|
91
|
+
homepageLinks: object[];
|
|
41
92
|
usedManifest: Set<string>;
|
|
42
93
|
};
|
|
43
94
|
/**
|
|
@@ -150,3 +150,31 @@ export function positionOfLiteral(text: string, needle: string, occurrence?: num
|
|
|
150
150
|
line?: number;
|
|
151
151
|
column?: number;
|
|
152
152
|
};
|
|
153
|
+
/**
|
|
154
|
+
* Where a **key or value in a YAML document** sits, addressed by its path.
|
|
155
|
+
*
|
|
156
|
+
* {@link positionOfLiteral} is the plain search, and it is the wrong tool for a
|
|
157
|
+
* configuration file: a pack name like `items` or `actors` appears in the
|
|
158
|
+
* `packs:` block, in a folder's list, in a path, and often in prose, so the
|
|
159
|
+
* first occurrence is routinely not the one the finding is about — which is the
|
|
160
|
+
* one thing the located form exists to prevent.
|
|
161
|
+
*
|
|
162
|
+
* A path resolves the exact node instead. The document is re-parsed here rather
|
|
163
|
+
* than threaded from the loader because the loader returns plain data: `yaml`
|
|
164
|
+
* discards ranges once a document is materialised, and carrying a parallel
|
|
165
|
+
* position tree through configuration resolution would be a second
|
|
166
|
+
* representation of the same file to keep in step.
|
|
167
|
+
*
|
|
168
|
+
* Every failure — unparseable text, an `.mjs` configuration, a path that
|
|
169
|
+
* resolves to nothing — yields `{}`, so a caller spreads the result and the
|
|
170
|
+
* position is dropped rather than guessed.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} text - The document's contents.
|
|
173
|
+
* @param {ReadonlyArray<string|number>} keyPath - Path to the node: map keys as
|
|
174
|
+
* strings, sequence entries as numbers.
|
|
175
|
+
* @returns {{line?: number, column?: number}} Spreadable position fields.
|
|
176
|
+
*/
|
|
177
|
+
export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string | number>): {
|
|
178
|
+
line?: number;
|
|
179
|
+
column?: number;
|
|
180
|
+
};
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* and an actor's embedded items may be sourced from any of them. Finding one
|
|
7
7
|
* pack and stopping is how embedded-item resolution would silently miss every
|
|
8
8
|
* item that landed in another. Returned in configured order, which is also the
|
|
9
|
-
* order they compile in
|
|
10
|
-
* is written
|
|
9
|
+
* order they compile in — {@link orderPassesByDependency} keeps the declared
|
|
10
|
+
* order among packs of one type — and every one of them is written before the
|
|
11
|
+
* actors pass that reads them.
|
|
11
12
|
*
|
|
12
13
|
* @param {object} [config] - The resolved build configuration. Defaults to this
|
|
13
14
|
* repository's.
|
|
@@ -16,6 +17,53 @@
|
|
|
16
17
|
* caller that needs one, refuses that itself.
|
|
17
18
|
*/
|
|
18
19
|
export function itemPackJsonDirs(config?: object): string[];
|
|
20
|
+
/**
|
|
21
|
+
* The passes to run, ordered so that each one follows the output it reads.
|
|
22
|
+
*
|
|
23
|
+
* **Declaration order is presentation, not compile order (#73).** The same
|
|
24
|
+
* `packs:` list is the manifest's `packs` array, which a consumer orders for a
|
|
25
|
+
* reader browsing compendiums; the actors pass, meanwhile, resolves each
|
|
26
|
+
* being's embedded items against the item passes' *output*. Making one list
|
|
27
|
+
* satisfy both meant an Actor pack declared first compiled only where a
|
|
28
|
+
* previous run had already left `build/packs-json` populated — green on a warm
|
|
29
|
+
* tree, exit 1 on every fresh checkout and every CI runner, and `build/` is
|
|
30
|
+
* gitignored so that is the state CI always starts from.
|
|
31
|
+
*
|
|
32
|
+
* **The reordering is the smallest one that works.** Each step takes the
|
|
33
|
+
* *earliest declared* pass whose dependencies are all already emitted, so a
|
|
34
|
+
* list that was already in a workable order comes back untouched, and one that
|
|
35
|
+
* was not moves exactly the passes that had to move. A dependency is satisfied
|
|
36
|
+
* only when **every** pack of that type has run: a being addresses an item by
|
|
37
|
+
* `(type, shortcode)` without knowing which Item pack ships it, so waiting for
|
|
38
|
+
* one of several would resolve some beings and silently fail others.
|
|
39
|
+
*
|
|
40
|
+
* A dependency on a type this configuration declares no pack of is not waited
|
|
41
|
+
* for. A package may ship an Actor pack and no Item pack; the pass that needs
|
|
42
|
+
* one refuses on its own, with a message about items rather than about order.
|
|
43
|
+
*
|
|
44
|
+
* @param {readonly object[]} packs - The passes to be run, as declared.
|
|
45
|
+
* @returns {object[]} A new list, in compile order. The input is untouched.
|
|
46
|
+
* @throws {Error} If the passes read each other's output in a cycle, which no
|
|
47
|
+
* order can satisfy. Only reachable from a mis-declared compiler, so it names
|
|
48
|
+
* the passes rather than blaming the pack list.
|
|
49
|
+
*/
|
|
50
|
+
export function orderPassesByDependency(packs: readonly object[]): object[];
|
|
51
|
+
/**
|
|
52
|
+
* The dependencies this run cannot satisfy by ordering, because the pass that
|
|
53
|
+
* would produce them is not in it.
|
|
54
|
+
*
|
|
55
|
+
* Ordering answers the whole-package build; a run restricted to one pack
|
|
56
|
+
* (`content-build package compile <name>`) cannot conjure the passes it left
|
|
57
|
+
* out. Where their output is already on disk from an earlier run that is fine
|
|
58
|
+
* — it is how compiling one pack at a time is meant to work — so this reports
|
|
59
|
+
* only what is genuinely absent, and names the pack that would write it rather
|
|
60
|
+
* than the directory that is missing.
|
|
61
|
+
*
|
|
62
|
+
* @param {readonly object[]} running - The passes this run will execute.
|
|
63
|
+
* @param {object} config - The resolved build configuration.
|
|
64
|
+
* @returns {string[]} One message per unsatisfiable dependency.
|
|
65
|
+
*/
|
|
66
|
+
export function unsatisfiedPassDependencies(running: readonly object[], config: object): string[];
|
|
19
67
|
/**
|
|
20
68
|
* The passes that compiled nothing when they were expected to compile
|
|
21
69
|
* something — a build failure, not a quiet no-op.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a note's frontmatter declares the homepage type.
|
|
3
|
+
*
|
|
4
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
5
|
+
* @returns {boolean} Whether it is a homepage note.
|
|
6
|
+
*/
|
|
7
|
+
export function isHomepage(fm: object | null | undefined): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The title a homepage publishes under.
|
|
10
|
+
*
|
|
11
|
+
* The one defaulted value on the page, and it defaults to the package's own
|
|
12
|
+
* `packageBuild.manifest.title` — the name Foundry already shows for the
|
|
13
|
+
* package — so a homepage that adds nothing to it need not restate it. An
|
|
14
|
+
* authored `title` wins, because a front page is allowed to greet a reader
|
|
15
|
+
* differently from a package browser.
|
|
16
|
+
*
|
|
17
|
+
* Falls back to `contentPackage` last, so a package that has no manifest of its
|
|
18
|
+
* own still yields a titled page rather than a blank heading.
|
|
19
|
+
*
|
|
20
|
+
* @param {object|null|undefined} fm - The note's frontmatter.
|
|
21
|
+
* @param {object} config - The resolved configuration.
|
|
22
|
+
* @returns {string} The title.
|
|
23
|
+
*/
|
|
24
|
+
export function homepageTitle(fm: object | null | undefined, config: object): string;
|
|
25
|
+
/**
|
|
26
|
+
* The frontmatter a homepage publishes with.
|
|
27
|
+
*
|
|
28
|
+
* The note's own, plus the two derived values every emitted page carries: the
|
|
29
|
+
* resolved `title`, and the package the build derived — no note declares one
|
|
30
|
+
* (`package:` is retired, #56) and the theme's breadcrumb partial reads
|
|
31
|
+
* `.Params.package`.
|
|
32
|
+
*
|
|
33
|
+
* An authored `aliases` is dropped for the same reason it is on every other
|
|
34
|
+
* page: Obsidian reads it as names a reader might call the note, Hugo reads it
|
|
35
|
+
* as URL redirects, and passing it through would publish a redirect stub at
|
|
36
|
+
* each one.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} fm - The note's frontmatter.
|
|
39
|
+
* @param {object} options - Options.
|
|
40
|
+
* @param {string} options.contentPackage - The package this build publishes.
|
|
41
|
+
* @param {string} options.title - The resolved title.
|
|
42
|
+
* @returns {object} The frontmatter to write.
|
|
43
|
+
*/
|
|
44
|
+
export function homepageFrontmatter(fm: object, { contentPackage, title }: {
|
|
45
|
+
contentPackage: string;
|
|
46
|
+
title: string;
|
|
47
|
+
}): object;
|
|
48
|
+
/**
|
|
49
|
+
* Every address a homepage carries, wherever it is written.
|
|
50
|
+
*
|
|
51
|
+
* **Both halves of the page are in scope, and that is the finding rather than
|
|
52
|
+
* the assumption.** Of the six homepages authored today, four carry every link
|
|
53
|
+
* in the body as ordinary markdown and two carry them in `landing:` — and the
|
|
54
|
+
* one whose dead links prompted the check has an *empty body*, so a body-only
|
|
55
|
+
* reading would have found nothing at all on it. A dead link in a card is
|
|
56
|
+
* exactly as broken as one in a paragraph.
|
|
57
|
+
*
|
|
58
|
+
* Three shapes are gathered, and the caller needs to tell them apart because
|
|
59
|
+
* the rules differ:
|
|
60
|
+
*
|
|
61
|
+
* - **`url`** — package-relative, resolved against the site by the theme.
|
|
62
|
+
* - **`href`** — already resolved, used verbatim.
|
|
63
|
+
* - **prose and body markdown links** — emitted as written and resolved by the
|
|
64
|
+
* browser against the landing's own address, which *is* the package root, so
|
|
65
|
+
* a relative one means the same thing a `url` does.
|
|
66
|
+
*
|
|
67
|
+
* `banner:` is not an address: it is an image path resolved through the CDN
|
|
68
|
+
* base, and `banner: none` is a sentinel rather than a target. Top-level
|
|
69
|
+
* `title` and `description` are not walked either — they are set as text, never
|
|
70
|
+
* rendered as markdown.
|
|
71
|
+
*
|
|
72
|
+
* @param {object|null|undefined} fm - The note's frontmatter.
|
|
73
|
+
* @param {string} [body] - The note's markdown body.
|
|
74
|
+
* @returns {Array<{field: string, url: string, kind: string}>} Every address,
|
|
75
|
+
* frontmatter first and then the body, each with the dotted path it was
|
|
76
|
+
* written at.
|
|
77
|
+
*/
|
|
78
|
+
export function homepageAddresses(fm: object | null | undefined, body?: string): Array<{
|
|
79
|
+
field: string;
|
|
80
|
+
url: string;
|
|
81
|
+
kind: string;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* The note type that compiles to the package homepage.
|
|
85
|
+
*
|
|
86
|
+
* @type {string}
|
|
87
|
+
*/
|
|
88
|
+
export const HOMEPAGE_TYPE: string;
|
|
89
|
+
/**
|
|
90
|
+
* What a homepage note may write under `sohl:` — nothing.
|
|
91
|
+
*
|
|
92
|
+
* Empty on purpose, and declared rather than omitted: a type with no vocabulary
|
|
93
|
+
* and a type that is unknown are different findings, and only the second is an
|
|
94
|
+
* authoring error. The whole envelope is the two top-level keys `type` and an
|
|
95
|
+
* optional `title`; there is no game-system data on a page that compiles to no
|
|
96
|
+
* document.
|
|
97
|
+
*
|
|
98
|
+
* @type {readonly import("./field-spec.mjs").FieldSpec[]}
|
|
99
|
+
*/
|
|
100
|
+
export const HOMEPAGE_FIELDS: readonly import("./field-spec.mjs").FieldSpec[];
|
|
101
|
+
/**
|
|
102
|
+
* Where a homepage is written, relative to the package's site root.
|
|
103
|
+
*
|
|
104
|
+
* Hugo's section landing, because the page *is* the package's landing: the
|
|
105
|
+
* package root is a section and this is its index.
|
|
106
|
+
*
|
|
107
|
+
* @type {string}
|
|
108
|
+
*/
|
|
109
|
+
export const HOMEPAGE_DESTINATION: string;
|
|
110
|
+
/**
|
|
111
|
+
* The two frontmatter keys that hold an address, and what each one means.
|
|
112
|
+
*
|
|
113
|
+
* They are **not** interchangeable, and a check that treated them as one would
|
|
114
|
+
* be wrong about both. The theme resolves a `url` against the site with
|
|
115
|
+
* `relURL`, so a package writes `kb/rules/` and is served `/sohl/kb/rules/`
|
|
116
|
+
* without ever naming its own prefix. An `href` is an address that is *already*
|
|
117
|
+
* resolved and is used verbatim — which is what `cards.source: sections` fills
|
|
118
|
+
* in, since a section's permalink already carries the prefix.
|
|
119
|
+
*
|
|
120
|
+
* So a leading `/` is a defect in a `url` (it is prefixed a second time) and
|
|
121
|
+
* correct in an `href`.
|
|
122
|
+
*
|
|
123
|
+
* @type {ReadonlySet<string>}
|
|
124
|
+
*/
|
|
125
|
+
export const HOMEPAGE_ADDRESS_KEYS: ReadonlySet<string>;
|
package/types/engine/index.d.mts
CHANGED
|
@@ -7,6 +7,8 @@ export * as packRouter from "./pack-router.mjs";
|
|
|
7
7
|
export * as contentPackage from "./content-package.mjs";
|
|
8
8
|
export * as notePackage from "./note-package.mjs";
|
|
9
9
|
export * as retiredFields from "./retired-fields.mjs";
|
|
10
|
+
export * as homepage from "./homepage.mjs";
|
|
11
|
+
export * as noteSchemas from "./note-schemas.mjs";
|
|
10
12
|
export * as contentSlug from "./content-slug.mjs";
|
|
11
13
|
export * as contentAddress from "./content-address.mjs";
|
|
12
14
|
export * as foreignManifests from "./foreign-manifests.mjs";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every engine-level content type, and what a note of that type may write.
|
|
3
|
+
*
|
|
4
|
+
* @type {Readonly<Record<string, readonly import("./field-spec.mjs").FieldSpec[]>>}
|
|
5
|
+
*/
|
|
6
|
+
export const ENGINE_NOTE_SCHEMAS: Readonly<Record<string, readonly import("./field-spec.mjs").FieldSpec[]>>;
|
|
@@ -56,6 +56,19 @@ export function configFromData(data: unknown, configPath: string): import("../co
|
|
|
56
56
|
* compilers know neither what to compile nor where to put it.
|
|
57
57
|
*/
|
|
58
58
|
export function loadPackConfig(): import("../config.mjs").ContentBuildConfig;
|
|
59
|
+
/**
|
|
60
|
+
* The file {@link loadPackConfig} resolved the configuration from.
|
|
61
|
+
*
|
|
62
|
+
* A diagnostic about a *configured* value has to name the file it was declared
|
|
63
|
+
* in, and re-deriving that path at the point of the finding would be a second
|
|
64
|
+
* resolution free to disagree with the first — the `PACKAGE_BUILD_CONFIG`
|
|
65
|
+
* override, the upward walk and the one-file-per-directory rule all have to
|
|
66
|
+
* come out the same way. This reports the path actually read.
|
|
67
|
+
*
|
|
68
|
+
* @returns {string} Its absolute path.
|
|
69
|
+
* @throws {Error} As {@link loadPackConfig}, when there is no configuration.
|
|
70
|
+
*/
|
|
71
|
+
export function packConfigPath(): string;
|
|
59
72
|
/** The stem every consuming repository declares its build under. */
|
|
60
73
|
export const CONFIG_BASENAME: "package-build.config";
|
|
61
74
|
/**
|
|
@@ -41,6 +41,49 @@ export function collectTreePages(tree: object, ctx: object): {
|
|
|
41
41
|
pages: object[];
|
|
42
42
|
fmLinkFindings: object[];
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* The package's homepage notes — the authored page at `/<contentPackage>/`.
|
|
46
|
+
*
|
|
47
|
+
* A separate walk from {@link collectContentPages} rather than a branch inside
|
|
48
|
+
* it, because in homepage-only mode it is the **whole** of the site build: the
|
|
49
|
+
* content tree is never read for pages at all, so the licensing constraint two
|
|
50
|
+
* packages ship under is a property of the code path rather than of a
|
|
51
|
+
* configuration that happens to be empty (#55).
|
|
52
|
+
*
|
|
53
|
+
* Returned as a list rather than as the one note there should be. Requiring
|
|
54
|
+
* exactly one is #52's, and it is a separate decision — this reports what it
|
|
55
|
+
* found so a count is visible either way.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} contentBase - Absolute path to the content tree.
|
|
58
|
+
* @param {object} ctx - `{ skipDirectories }`.
|
|
59
|
+
* @returns {{pages: object[]}} The homepage notes, in walk order.
|
|
60
|
+
*/
|
|
61
|
+
export function collectHomepages(contentBase: string, ctx: object): {
|
|
62
|
+
pages: object[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Writes each homepage at the package's own root.
|
|
66
|
+
*
|
|
67
|
+
* Its own writer, deliberately small. A homepage is authored markdown published
|
|
68
|
+
* verbatim — no table expansion, no section landing and no link resolution — so
|
|
69
|
+
* routing it through {@link renderPages} would buy it a pipeline it has no input
|
|
70
|
+
* for, and would make homepage-only mode depend on the index, the foreign
|
|
71
|
+
* manifests and the table universe that mode exists to not build.
|
|
72
|
+
*
|
|
73
|
+
* **Verbatim is the answer to #54, not a gap left by it.** A landing's links
|
|
74
|
+
* could not be *resolved* here without giving `homepage` mode the index its
|
|
75
|
+
* licensing fence exists to not build, so they are **checked** instead:
|
|
76
|
+
* {@link auditHomepageLinks} reads the `landing:` addresses and the body's
|
|
77
|
+
* markdown links, and reports a wikilink on the page rather than resolving one.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} outRoot - The package's site root — the configured `site.out`,
|
|
80
|
+
* one level above the content mount.
|
|
81
|
+
* @param {readonly object[]} pages - From {@link collectHomepages}.
|
|
82
|
+
* @param {object} config - The resolved configuration, for the package name and
|
|
83
|
+
* the default title.
|
|
84
|
+
* @returns {number} How many pages were written.
|
|
85
|
+
*/
|
|
86
|
+
export function writeHomepages(outRoot: string, pages: readonly object[], config: object): number;
|
|
44
87
|
/**
|
|
45
88
|
* The integrity gates a site build runs before it writes anything.
|
|
46
89
|
*
|
|
@@ -63,6 +106,17 @@ export function collectTreePages(tree: object, ctx: object): {
|
|
|
63
106
|
* @returns {object} The gate results and, when they pass, the built index.
|
|
64
107
|
*/
|
|
65
108
|
export function siteGates(pages: object[], findings: object, { manifestDir }: object): object;
|
|
109
|
+
/**
|
|
110
|
+
* The gate result of a build that ran none of them.
|
|
111
|
+
*
|
|
112
|
+
* Homepage-only publishes one authored page and resolves nothing, so every gate
|
|
113
|
+
* here is about a surface that mode does not have. The shape is returned all the
|
|
114
|
+
* same, because a caller reads the same fields whichever mode ran and a `null`
|
|
115
|
+
* would make each of them a special case.
|
|
116
|
+
*
|
|
117
|
+
* @returns {object} An all-clear gate result.
|
|
118
|
+
*/
|
|
119
|
+
export function emptyGates(): object;
|
|
66
120
|
/** Whether any gate produced a finding. */
|
|
67
121
|
export function gatesFailed(gates: any): boolean;
|
|
68
122
|
/**
|
package/types/manifest.d.mts
CHANGED
|
@@ -59,6 +59,60 @@ export function releaseUrls({ repoUrl, version, artifact }: {
|
|
|
59
59
|
* @returns {object[]} The manifest's `packs` array.
|
|
60
60
|
*/
|
|
61
61
|
export function manifestPacks(config: object): object[];
|
|
62
|
+
/**
|
|
63
|
+
* What `packFolders` and the derived `packs[]` disagree about.
|
|
64
|
+
*
|
|
65
|
+
* `packFolders` is the one **declared** manifest key that names something the
|
|
66
|
+
* build **derives**: every other declared key states a fact about the package
|
|
67
|
+
* (`title`, `socket`, `grid`) or addresses a staged file (`esmodules`,
|
|
68
|
+
* `styles`, `languages`), and a staged file is a different relation, checked
|
|
69
|
+
* against the stage rather than against configuration. So this is the one place
|
|
70
|
+
* a declaration can go stale against a value the build already computed — and
|
|
71
|
+
* until now nothing compared them (#81).
|
|
72
|
+
*
|
|
73
|
+
* `HarnMaster-3-FoundryVTT` shipped the consequence: its folder named four
|
|
74
|
+
* packs, three of which had not existed since the compendium was consolidated,
|
|
75
|
+
* and omitted `items` — 1,577 of 1,597 documents, loose in Foundry's compendium
|
|
76
|
+
* browser, with the build reporting nothing (HM3#420).
|
|
77
|
+
*
|
|
78
|
+
* **The two findings are not the same finding**, and giving them one severity
|
|
79
|
+
* gets one of them wrong:
|
|
80
|
+
*
|
|
81
|
+
* - _A folder names a pack that does not exist_ is an **error**. Foundry
|
|
82
|
+
* resolves the name against the package's own packs and silently skips what
|
|
83
|
+
* it cannot find, so the declaration does nothing at all; there is no
|
|
84
|
+
* arrangement in which it is intended, and the fix is unambiguous.
|
|
85
|
+
* - _A pack no folder names_ is a **warning**. It is legal and can be
|
|
86
|
+
* deliberate — a package may want one pack at the root — so failing on it
|
|
87
|
+
* would break working packages for a matter of taste. But a package that
|
|
88
|
+
* bothered to declare a folder rarely meant to leave one out, which is
|
|
89
|
+
* exactly how HM3's `items` went unnoticed.
|
|
90
|
+
* - _A package declaring no folders_ says **nothing**. Everything at the root
|
|
91
|
+
* is the majority arrangement, not an omission.
|
|
92
|
+
*
|
|
93
|
+
* Errors come first, in declaration order, then warnings in pack order: the
|
|
94
|
+
* unresolvable names are what a reader fixes, and a folder gaining a name often
|
|
95
|
+
* settles a warning too.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} options
|
|
98
|
+
* @param {unknown} [options.packFolders] - The declared `packFolders`.
|
|
99
|
+
* @param {ReadonlyArray<{name: string}>} [options.packs] - The derived packs,
|
|
100
|
+
* as {@link manifestPacks} returns them.
|
|
101
|
+
* @returns {Array<{severity: "error"|"warning", message: string, pack: string,
|
|
102
|
+
* folder?: string, keyPath: Array<string|number>}>} The findings, ordered.
|
|
103
|
+
*/
|
|
104
|
+
export function packFolderFindings({ packFolders, packs }: {
|
|
105
|
+
packFolders?: unknown;
|
|
106
|
+
packs?: readonly {
|
|
107
|
+
name: string;
|
|
108
|
+
}[] | undefined;
|
|
109
|
+
}): Array<{
|
|
110
|
+
severity: "error" | "warning";
|
|
111
|
+
message: string;
|
|
112
|
+
pack: string;
|
|
113
|
+
folder?: string;
|
|
114
|
+
keyPath: Array<string | number>;
|
|
115
|
+
}>;
|
|
62
116
|
/**
|
|
63
117
|
* The `relationships` block as published — every declared dependency, with the
|
|
64
118
|
* build's own keys dropped.
|
|
@@ -109,20 +163,32 @@ export function buildManifest({ config, packageJson, artifact, flags }: {
|
|
|
109
163
|
/**
|
|
110
164
|
* Write the generated manifest into the staged package.
|
|
111
165
|
*
|
|
166
|
+
* The declared `packFolders` is checked against the derived `packs[]` first,
|
|
167
|
+
* and an unresolvable name **stops the write**: a manifest already known to
|
|
168
|
+
* describe packs the package does not ship should not reach the stage, where
|
|
169
|
+
* the next command would deploy it (#81). See {@link packFolderFindings} for
|
|
170
|
+
* the rule and why its two findings carry different severities.
|
|
171
|
+
*
|
|
112
172
|
* @param {object} options - As {@link buildManifest}, plus where to write.
|
|
113
173
|
* @param {object} options.config - The resolved content configuration.
|
|
114
174
|
* @param {object} options.packageJson - The repository's `package.json`.
|
|
115
175
|
* @param {string} options.artifact - `system` or `module`.
|
|
116
176
|
* @param {string} options.outDir - Directory to write into.
|
|
117
177
|
* @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
|
|
178
|
+
* @param {string} [options.configFile] - Absolute path of the configuration
|
|
179
|
+
* file the manifest was resolved from, so a finding about it can be located.
|
|
180
|
+
* Omitting it costs the position, not the finding.
|
|
118
181
|
* @returns {Promise<{path: string, manifest: object}>} Where it went, and what.
|
|
182
|
+
* @throws {Error} When a `packFolders` entry names a pack the package does not
|
|
183
|
+
* ship. Nothing is written in that case.
|
|
119
184
|
*/
|
|
120
|
-
export function writeManifest({ config, packageJson, artifact, outDir, flags, }: {
|
|
185
|
+
export function writeManifest({ config, packageJson, artifact, outDir, flags, configFile, }: {
|
|
121
186
|
config: object;
|
|
122
187
|
packageJson: object;
|
|
123
188
|
artifact: string;
|
|
124
189
|
outDir: string;
|
|
125
190
|
flags?: Record<string, object> | undefined;
|
|
191
|
+
configFile?: string | undefined;
|
|
126
192
|
}): Promise<{
|
|
127
193
|
path: string;
|
|
128
194
|
manifest: object;
|