@heroiclands/package-build 22.0.3 → 22.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 +66 -0
- package/CONTENT.md +56 -18
- package/README.md +1 -1
- package/bin/content-build.mjs +67 -23
- package/bin/package-build.mjs +3 -2
- package/config.mjs +52 -1
- package/content-config.mjs +264 -8
- package/docs/api.md +33 -30
- package/docs/commands.md +68 -32
- package/docs/configuration.md +276 -62
- package/docs/getting-started.md +15 -4
- package/docs/project-setup.md +45 -20
- package/engine/content-links.mjs +19 -9
- package/engine/helpers.mjs +2 -1
- package/engine/metadata-index.mjs +32 -0
- package/engine/note-vocabulary.mjs +20 -0
- package/engine/pack-config.mjs +21 -2
- package/engine/site-build.mjs +29 -55
- package/engine/site-config.mjs +503 -0
- package/engine/site-index.mjs +10 -1
- package/engine/web-wikilinks.mjs +16 -6
- package/engine/wikilink-syntax.mjs +10 -0
- package/engine/wikilinks.mjs +34 -7
- package/manifest.mjs +12 -7
- package/package.json +2 -1
- package/stage.mjs +4 -3
- package/types/config.d.mts +24 -0
- package/types/content-config.d.mts +64 -0
- package/types/engine/content-links.d.mts +3 -2
- package/types/engine/metadata-index.d.mts +22 -0
- package/types/engine/note-vocabulary.d.mts +12 -0
- package/types/engine/site-build.d.mts +8 -26
- package/types/engine/site-config.d.mts +236 -0
- package/types/engine/site-index.d.mts +6 -1
- package/types/engine/web-wikilinks.d.mts +9 -5
- package/types/engine/wikilink-syntax.d.mts +3 -0
- package/types/engine/wikilinks.d.mts +16 -5
- package/types/manifest.d.mts +4 -4
- package/types/stage.d.mts +4 -3
package/content-config.mjs
CHANGED
|
@@ -175,6 +175,10 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
|
|
|
175
175
|
// dependency — so nesting one under the other would imply a containment
|
|
176
176
|
// that does not hold.
|
|
177
177
|
metadataCache: "build/cache/metadata",
|
|
178
|
+
// Where the site navigation heroiclands.org publishes is fetched to.
|
|
179
|
+
// Beside the other two: it is fetched by the same command and read under
|
|
180
|
+
// the same complete-marker rule.
|
|
181
|
+
navigationCache: "build/cache/navigation",
|
|
178
182
|
});
|
|
179
183
|
|
|
180
184
|
/**
|
|
@@ -387,6 +391,8 @@ export function publishesContentPages(config) {
|
|
|
387
391
|
* index is fetched to. Inbound,
|
|
388
392
|
* for *every* declared dependency, not
|
|
389
393
|
* only those supplying a catalogue.
|
|
394
|
+
* @property {string} [navigationCache] Where the site navigation is fetched
|
|
395
|
+
* to, for the generated Hugo menu.
|
|
390
396
|
*/
|
|
391
397
|
|
|
392
398
|
/**
|
|
@@ -401,6 +407,7 @@ export function publishesContentPages(config) {
|
|
|
401
407
|
* @property {string} unpack
|
|
402
408
|
* @property {string} foreignCache
|
|
403
409
|
* @property {string} metadataCache
|
|
410
|
+
* @property {string} navigationCache
|
|
404
411
|
*/
|
|
405
412
|
|
|
406
413
|
/**
|
|
@@ -496,6 +503,14 @@ export function publishesContentPages(config) {
|
|
|
496
503
|
* package this one targets — for a system
|
|
497
504
|
* relationship, `verified` is what
|
|
498
505
|
* `_stats.systemVersion` is stamped from.
|
|
506
|
+
* @property {boolean} [contentIndex] Whether `deps fetch` fetches this
|
|
507
|
+
* dependency's content index. Default
|
|
508
|
+
* `true`. `false` declares the dependency
|
|
509
|
+
* for the Foundry manifest only — nothing
|
|
510
|
+
* this tree cites by wikilink — and refuses
|
|
511
|
+
* `itemCatalog: true` on the same entry,
|
|
512
|
+
* since a catalogue is fetched from the same
|
|
513
|
+
* index.
|
|
499
514
|
*/
|
|
500
515
|
|
|
501
516
|
/**
|
|
@@ -578,6 +593,13 @@ export function publishesContentPages(config) {
|
|
|
578
593
|
* Refused by a `documentation`
|
|
579
594
|
* package, which ships no Foundry
|
|
580
595
|
* package.
|
|
596
|
+
* @property {string} [homepage] `package.json`'s own `homepage` —
|
|
597
|
+
* the site build's `baseURL`. Checked
|
|
598
|
+
* by `checkHomepage` in
|
|
599
|
+
* `config.mjs`.
|
|
600
|
+
* @property {string|{name: string, email?: string, url?: string}} [author]
|
|
601
|
+
* `package.json`'s own `author`, in
|
|
602
|
+
* either of npm's forms.
|
|
581
603
|
* @property {PackageKind} packageKind Whether the package is a system, a
|
|
582
604
|
* module, or documentation — the kind
|
|
583
605
|
* that publishes a site and a book
|
|
@@ -637,6 +659,13 @@ export function publishesContentPages(config) {
|
|
|
637
659
|
* @property {string} contentPackage
|
|
638
660
|
* @property {string|null} foundryPackage `null` for a `documentation`
|
|
639
661
|
* package, which ships no Foundry package.
|
|
662
|
+
* @property {string|null} homepage `package.json`'s own `homepage`,
|
|
663
|
+
* checked by `checkHomepage` in
|
|
664
|
+
* `config.mjs`.
|
|
665
|
+
* @property {Readonly<{name: string, email?: string, url?: string}>|null} author
|
|
666
|
+
* `package.json`'s own `author`, normalised
|
|
667
|
+
* from either of npm's forms; `null` when
|
|
668
|
+
* the package declares none.
|
|
640
669
|
* @property {PackageKind} packageKind
|
|
641
670
|
* @property {string|null} assetRoot Derived, and **conditional**: the served
|
|
642
671
|
* Foundry asset root,
|
|
@@ -705,6 +734,8 @@ const CONFIG_KEYS = [
|
|
|
705
734
|
"rootDir",
|
|
706
735
|
"contentPackage",
|
|
707
736
|
"foundryPackage",
|
|
737
|
+
"homepage",
|
|
738
|
+
"author",
|
|
708
739
|
"packageKind",
|
|
709
740
|
"stats",
|
|
710
741
|
"itemBuilders",
|
|
@@ -726,7 +757,6 @@ const SYSTEM_KEYS = ["manifest", "compatibility"];
|
|
|
726
757
|
const COMPATIBILITY_KEYS = ["minimum", "verified"];
|
|
727
758
|
const DOCS_KEYS = ["itemFields"];
|
|
728
759
|
const SITE_KEYS = [
|
|
729
|
-
"out",
|
|
730
760
|
"base",
|
|
731
761
|
"assets",
|
|
732
762
|
"packages",
|
|
@@ -737,8 +767,14 @@ const SITE_KEYS = [
|
|
|
737
767
|
"pass",
|
|
738
768
|
"passOptions",
|
|
739
769
|
"backfillSections",
|
|
770
|
+
"list",
|
|
771
|
+
"notfound",
|
|
772
|
+
"hugo",
|
|
740
773
|
];
|
|
741
774
|
const SITE_TREE_KEYS = ["from", "section"];
|
|
775
|
+
const SITE_LIST_KEYS = ["shortcodes"];
|
|
776
|
+
const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
|
|
777
|
+
const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
|
|
742
778
|
const PDF_KEYS = ["title", "subtitle", "document", "out", "front", "fonts", "iconFonts", "binary"];
|
|
743
779
|
const PDF_FONT_KEYS = ["serif", "sans", "mono", "path"];
|
|
744
780
|
const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: "" });
|
|
@@ -752,7 +788,9 @@ const RELATIONSHIP_KEYS = [
|
|
|
752
788
|
"manifest",
|
|
753
789
|
"compatibility",
|
|
754
790
|
"itemCatalog",
|
|
791
|
+
"contentIndex",
|
|
755
792
|
];
|
|
793
|
+
const AUTHOR_KEYS = ["name", "email", "url"];
|
|
756
794
|
const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
|
|
757
795
|
const ITEM_REGISTRY_KEYS = ["system", "builders"];
|
|
758
796
|
const PACK_KEYS = [
|
|
@@ -964,6 +1002,56 @@ function optionalString(value, field) {
|
|
|
964
1002
|
return value;
|
|
965
1003
|
}
|
|
966
1004
|
|
|
1005
|
+
/**
|
|
1006
|
+
* npm's `author` field, in either of its two forms.
|
|
1007
|
+
*
|
|
1008
|
+
* `package.json` accepts a single string — `"Name <email> (url)"`, with the
|
|
1009
|
+
* email and the URL both optional — or an object carrying the same three
|
|
1010
|
+
* parts. Both normalise to one shape, so the site build reads one field
|
|
1011
|
+
* instead of branching on which form a repository happened to write.
|
|
1012
|
+
*
|
|
1013
|
+
* @type {RegExp}
|
|
1014
|
+
*/
|
|
1015
|
+
const AUTHOR_STRING = /^([^<(]*?)\s*(?:<([^>]*)>)?\s*(?:\(([^)]*)\))?\s*$/;
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* @param {unknown} value - The declared `author`, or `undefined`.
|
|
1019
|
+
* @returns {Readonly<{name: string, email?: string, url?: string}>|null}
|
|
1020
|
+
* `null` when the package declares none.
|
|
1021
|
+
*/
|
|
1022
|
+
function normalizeAuthor(value) {
|
|
1023
|
+
if (value === undefined) return null;
|
|
1024
|
+
if (typeof value === "string") {
|
|
1025
|
+
const match = AUTHOR_STRING.exec(value.trim());
|
|
1026
|
+
const name = match?.[1]?.trim();
|
|
1027
|
+
if (!match || !name) {
|
|
1028
|
+
fail(
|
|
1029
|
+
"author",
|
|
1030
|
+
'must be `"Name"`, `"Name <email>"`, `"Name (url)"` or ' +
|
|
1031
|
+
'`"Name <email> (url)"` — npm\'s own `author` forms',
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
return Object.freeze({
|
|
1035
|
+
name: /** @type {string} */ (name),
|
|
1036
|
+
...(match[2] ? { email: match[2] } : {}),
|
|
1037
|
+
...(match[3] ? { url: match[3] } : {}),
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
if (!isPlainObject(value)) {
|
|
1041
|
+
fail("author", "must be a string or an object with `name`, `email` and `url`");
|
|
1042
|
+
}
|
|
1043
|
+
const author = /** @type {Record<string, unknown>} */ (value);
|
|
1044
|
+
rejectUnknownKeys(author, AUTHOR_KEYS, "author.");
|
|
1045
|
+
const name = requireNonEmptyString(author.name, "author.name");
|
|
1046
|
+
return Object.freeze({
|
|
1047
|
+
name,
|
|
1048
|
+
...(author.email !== undefined ?
|
|
1049
|
+
{ email: optionalString(author.email, "author.email") }
|
|
1050
|
+
: {}),
|
|
1051
|
+
...(author.url !== undefined ? { url: optionalString(author.url, "author.url") } : {}),
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
967
1055
|
/**
|
|
968
1056
|
* @param {unknown} value
|
|
969
1057
|
* @param {string} where Field path used in error messages.
|
|
@@ -1504,21 +1592,151 @@ function normalizeSectionMap(value, where) {
|
|
|
1504
1592
|
return Object.freeze(out);
|
|
1505
1593
|
}
|
|
1506
1594
|
|
|
1595
|
+
/**
|
|
1596
|
+
* Hugo keys a repository may **not** declare under `site.hugo`, because the
|
|
1597
|
+
* site build generates them and would only overwrite what was written.
|
|
1598
|
+
*
|
|
1599
|
+
* The same rule `DERIVED_MANIFEST_KEYS` states for the manifest, for the same
|
|
1600
|
+
* reason: an authored `baseURL` would look authoritative, sit there unread,
|
|
1601
|
+
* and disagree with the site forever. Each key names where its value comes
|
|
1602
|
+
* from. A dotted key names a nested one, and covers everything beneath it —
|
|
1603
|
+
* `params.brand` refuses `params.brand.logo` too — so `site.hugo` reaches only
|
|
1604
|
+
* what the generator does not write.
|
|
1605
|
+
*
|
|
1606
|
+
* @type {Readonly<Record<string, string>>}
|
|
1607
|
+
*/
|
|
1608
|
+
export const DERIVED_HUGO_KEYS = Object.freeze({
|
|
1609
|
+
baseURL: "package.json `homepage`",
|
|
1610
|
+
title: "`packageBuild.manifest.title`",
|
|
1611
|
+
locale: "the organisation's locale, in `engine/site-config.mjs`",
|
|
1612
|
+
publishDir: "`contentPackage`, under the deployment root `build/site`",
|
|
1613
|
+
contentDir: "the fixed content mount, `build/hugo/content`",
|
|
1614
|
+
themesDir: "where `@heroiclands/hugo-theme` is installed",
|
|
1615
|
+
theme: "the installed `@heroiclands/hugo-theme`",
|
|
1616
|
+
disableKinds: "whether any note in the tree carries `tags:`, which the site walk discovers",
|
|
1617
|
+
taxonomies: "whether any note in the tree carries `tags:`, which the site walk discovers",
|
|
1618
|
+
outputs: "whether any note in the tree carries `tags:`, which the site walk discovers",
|
|
1619
|
+
"params.description": "package.json `description`",
|
|
1620
|
+
"params.author": "package.json `author`",
|
|
1621
|
+
"params.cdnBaseURL": "`site.assets`",
|
|
1622
|
+
"params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
|
|
1623
|
+
"params.list": "`site.list`",
|
|
1624
|
+
"params.notfound": "`site.notfound`",
|
|
1625
|
+
"markup.goldmark.renderer.unsafe": "the toolchain, whose pages carry raw HTML",
|
|
1626
|
+
menu: "the navigation `content-build deps fetch` caches from heroiclands.org",
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* The `site.list` block — how a listing page renders.
|
|
1631
|
+
*
|
|
1632
|
+
* @param {unknown} value - The block, or `undefined`.
|
|
1633
|
+
* @returns {Readonly<{shortcodes: boolean}>} It, frozen, with every default filled.
|
|
1634
|
+
*/
|
|
1635
|
+
function normalizeSiteList(value) {
|
|
1636
|
+
if (value === undefined) return Object.freeze({ shortcodes: false });
|
|
1637
|
+
if (!isPlainObject(value)) fail("site.list", "must be a mapping");
|
|
1638
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1639
|
+
rejectUnknownKeys(input, SITE_LIST_KEYS, "site.list.");
|
|
1640
|
+
return Object.freeze({
|
|
1641
|
+
shortcodes: optionalBoolean(input.shortcodes, "site.list.shortcodes", false),
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* The `site.notfound` block — the wording of the "page not found" page.
|
|
1647
|
+
*
|
|
1648
|
+
* The theme renders the page for every site; what a repository supplies is
|
|
1649
|
+
* the tagline, the noun the body prose calls the site, and the routes back.
|
|
1650
|
+
* `tagline` and `sitenoun` are required once the block is present: a block
|
|
1651
|
+
* declaring only links would render the theme's generic wording above a list
|
|
1652
|
+
* of this site's routes, which reads as two sites.
|
|
1653
|
+
*
|
|
1654
|
+
* @param {unknown} value - The block, or `undefined`.
|
|
1655
|
+
* @returns {Readonly<object>|null} It, frozen; `null` when absent.
|
|
1656
|
+
*/
|
|
1657
|
+
function normalizeSiteNotfound(value) {
|
|
1658
|
+
if (value === undefined) return null;
|
|
1659
|
+
if (!isPlainObject(value)) fail("site.notfound", "must be a mapping");
|
|
1660
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1661
|
+
rejectUnknownKeys(input, SITE_NOTFOUND_KEYS, "site.notfound.");
|
|
1662
|
+
const out = {
|
|
1663
|
+
tagline: requireNonEmptyString(input.tagline, "site.notfound.tagline"),
|
|
1664
|
+
sitenoun: requireNonEmptyString(input.sitenoun, "site.notfound.sitenoun"),
|
|
1665
|
+
};
|
|
1666
|
+
if (input.heroimage !== undefined) {
|
|
1667
|
+
out.heroimage = requireNonEmptyString(input.heroimage, "site.notfound.heroimage");
|
|
1668
|
+
}
|
|
1669
|
+
if (input.links !== undefined) {
|
|
1670
|
+
if (!Array.isArray(input.links)) fail("site.notfound.links", "must be a list");
|
|
1671
|
+
out.links = Object.freeze(
|
|
1672
|
+
input.links.map((link, i) => {
|
|
1673
|
+
const where = `site.notfound.links[${i}]`;
|
|
1674
|
+
if (!isPlainObject(link)) fail(where, "must be a mapping");
|
|
1675
|
+
const entry = /** @type {Record<string, unknown>} */ (link);
|
|
1676
|
+
rejectUnknownKeys(entry, SITE_NOTFOUND_LINK_KEYS, `${where}.`);
|
|
1677
|
+
return Object.freeze({
|
|
1678
|
+
title: requireNonEmptyString(entry.title, `${where}.title`),
|
|
1679
|
+
url: requireNonEmptyString(entry.url, `${where}.url`),
|
|
1680
|
+
text: requireNonEmptyString(entry.text, `${where}.text`),
|
|
1681
|
+
});
|
|
1682
|
+
}),
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
return Object.freeze(out);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* The `site.hugo` block — a mapping deep-merged over the generated Hugo
|
|
1690
|
+
* configuration, last.
|
|
1691
|
+
*
|
|
1692
|
+
* The escape hatch for the key nobody anticipated. Every key the generator
|
|
1693
|
+
* writes is refused here by {@link DERIVED_HUGO_KEYS}, naming its source, so
|
|
1694
|
+
* the block cannot grow into a second configuration file.
|
|
1695
|
+
*
|
|
1696
|
+
* @param {unknown} value - The block, or `undefined`.
|
|
1697
|
+
* @returns {Readonly<Record<string, unknown>>} It, frozen; `{}` when absent.
|
|
1698
|
+
*/
|
|
1699
|
+
function normalizeSiteHugo(value) {
|
|
1700
|
+
if (value === undefined) return Object.freeze({});
|
|
1701
|
+
if (!isPlainObject(value)) fail("site.hugo", "must be a mapping");
|
|
1702
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1703
|
+
for (const [dotted, source] of Object.entries(DERIVED_HUGO_KEYS)) {
|
|
1704
|
+
/** @type {unknown} */
|
|
1705
|
+
let at = input;
|
|
1706
|
+
for (const part of dotted.split(".")) {
|
|
1707
|
+
at = isPlainObject(at) ? /** @type {Record<string, unknown>} */ (at)[part] : undefined;
|
|
1708
|
+
if (at === undefined) break;
|
|
1709
|
+
}
|
|
1710
|
+
if (at !== undefined) {
|
|
1711
|
+
fail(
|
|
1712
|
+
`site.hugo.${dotted}`,
|
|
1713
|
+
`is derived from ${source} and must not be declared — it ` +
|
|
1714
|
+
`would be overwritten, and the two would disagree with ` +
|
|
1715
|
+
`nothing to say so`,
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return Object.freeze(structuredClone(input));
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1507
1722
|
/**
|
|
1508
1723
|
* The `site` section — how this repository frames the website it publishes.
|
|
1509
1724
|
*
|
|
1510
|
-
* Everything here is *framing*:
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1725
|
+
* Everything here is *framing*: what a section is called, which extra trees
|
|
1726
|
+
* are published beside the content, which named pass bundle supplies the
|
|
1727
|
+
* repository's own body rewrites, and the residue of the generated Hugo
|
|
1728
|
+
* configuration that is genuinely this repository's own. Where the Hugo tree
|
|
1729
|
+
* is written is not a choice: `content-build site` writes it under
|
|
1730
|
+
* `build/hugo/`, and a `site.out` is refused by name. How a page gets its
|
|
1731
|
+
* **address** is deliberately not here either — that is `publish.address`,
|
|
1732
|
+
* shared with the link manifest so the two cannot disagree about where a
|
|
1733
|
+
* page is.
|
|
1515
1734
|
*
|
|
1516
1735
|
* @param {unknown} value - The `site` block, or `undefined`.
|
|
1517
1736
|
* @returns {Readonly<object>} It, frozen, with every default filled.
|
|
1518
1737
|
*/
|
|
1519
1738
|
function normalizeSite(value) {
|
|
1520
1739
|
const empty = Object.freeze({
|
|
1521
|
-
out: "",
|
|
1522
1740
|
base: "",
|
|
1523
1741
|
assets: "",
|
|
1524
1742
|
packages: Object.freeze([]),
|
|
@@ -1529,10 +1747,23 @@ function normalizeSite(value) {
|
|
|
1529
1747
|
pass: "",
|
|
1530
1748
|
passOptions: Object.freeze({}),
|
|
1531
1749
|
backfillSections: false,
|
|
1750
|
+
list: Object.freeze({ shortcodes: false }),
|
|
1751
|
+
notfound: null,
|
|
1752
|
+
hugo: Object.freeze({}),
|
|
1532
1753
|
});
|
|
1533
1754
|
if (value === undefined) return empty;
|
|
1534
1755
|
if (!isPlainObject(value)) fail("site", "must be a mapping");
|
|
1535
1756
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1757
|
+
// Refused by name, ahead of the vocabulary check: the useful thing to say
|
|
1758
|
+
// is not "no such key" but that the location is fixed.
|
|
1759
|
+
if (input.out !== undefined) {
|
|
1760
|
+
fail(
|
|
1761
|
+
"site.out",
|
|
1762
|
+
"is retired — the site build writes its content mount at " +
|
|
1763
|
+
"`build/hugo/content`, beside the generated `hugo.toml`, and " +
|
|
1764
|
+
"the location is not configurable. Remove the key",
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1536
1767
|
rejectUnknownKeys(input, SITE_KEYS, "site.");
|
|
1537
1768
|
|
|
1538
1769
|
const trees = [];
|
|
@@ -1574,7 +1805,6 @@ function normalizeSite(value) {
|
|
|
1574
1805
|
}
|
|
1575
1806
|
|
|
1576
1807
|
return Object.freeze({
|
|
1577
|
-
out: input.out === undefined ? "" : requireNonEmptyString(input.out, "site.out"),
|
|
1578
1808
|
base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
|
|
1579
1809
|
assets: normalizeSiteAssets(input.assets),
|
|
1580
1810
|
packages: Object.freeze(packages),
|
|
@@ -1588,6 +1818,9 @@ function normalizeSite(value) {
|
|
|
1588
1818
|
Object.freeze({})
|
|
1589
1819
|
: Object.freeze({ ...input.passOptions }),
|
|
1590
1820
|
backfillSections: optionalBoolean(input.backfillSections, "site.backfillSections", false),
|
|
1821
|
+
list: normalizeSiteList(input.list),
|
|
1822
|
+
notfound: normalizeSiteNotfound(input.notfound),
|
|
1823
|
+
hugo: normalizeSiteHugo(input.hugo),
|
|
1591
1824
|
});
|
|
1592
1825
|
}
|
|
1593
1826
|
|
|
@@ -1925,6 +2158,24 @@ function normalizeRelationships(value) {
|
|
|
1925
2158
|
}
|
|
1926
2159
|
spec.itemCatalog = rel.itemCatalog;
|
|
1927
2160
|
}
|
|
2161
|
+
// Opt-out: declares the dependency for the Foundry manifest
|
|
2162
|
+
// only, so `deps fetch` fetches no content index for it and a
|
|
2163
|
+
// wikilink into it is refused rather than silently dead. A
|
|
2164
|
+
// catalogue is fetched from the same index, so it cannot be
|
|
2165
|
+
// declared alongside `itemCatalog: true`.
|
|
2166
|
+
if (rel.contentIndex !== undefined) {
|
|
2167
|
+
if (typeof rel.contentIndex !== "boolean") {
|
|
2168
|
+
fail(`${at}.contentIndex`, "must be true or false");
|
|
2169
|
+
}
|
|
2170
|
+
if (rel.contentIndex === false && spec.itemCatalog) {
|
|
2171
|
+
fail(
|
|
2172
|
+
`${at}.contentIndex`,
|
|
2173
|
+
"cannot be false together with `itemCatalog: true` — a catalogue is " +
|
|
2174
|
+
"fetched from the same index",
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
spec.contentIndex = rel.contentIndex;
|
|
2178
|
+
}
|
|
1928
2179
|
return Object.freeze(spec);
|
|
1929
2180
|
}),
|
|
1930
2181
|
);
|
|
@@ -2493,6 +2744,11 @@ export function defineConfig(config) {
|
|
|
2493
2744
|
rootDir,
|
|
2494
2745
|
contentPackage: requireContentPackage(input.contentPackage, docEntryTypes),
|
|
2495
2746
|
foundryPackage,
|
|
2747
|
+
// `package.json`'s own address and byline. `homepage` is checked by
|
|
2748
|
+
// `checkHomepage` in `config.mjs`.
|
|
2749
|
+
homepage:
|
|
2750
|
+
input.homepage === undefined ? null : requireNonEmptyString(input.homepage, "homepage"),
|
|
2751
|
+
author: normalizeAuthor(input.author),
|
|
2496
2752
|
packageKind: /** @type {PackageKind} */ (packageKind),
|
|
2497
2753
|
// Foundry serves a package's files from `<kind>/<id>/`, so this is the
|
|
2498
2754
|
// one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
|
package/docs/api.md
CHANGED
|
@@ -342,6 +342,7 @@ The closed half of a note's frontmatter: the `data:` container and each type's `
|
|
|
342
342
|
| `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
|
|
343
343
|
| `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading the single-valued tag slots a note type has, such as a being's kind |
|
|
344
344
|
| `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
|
|
345
|
+
| `hasAnyTag` | `hasAnyTag(fm)` | `boolean` | checking whether a note carries any `tags:` at all, for the site build's taxonomy decision |
|
|
345
346
|
| `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
|
|
346
347
|
| `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
|
|
347
348
|
| `typeCharsetMessage` | `typeCharsetMessage(type)` | `string` | building the message for a `type` outside the address charset |
|
|
@@ -491,21 +492,22 @@ The shortcodes a note declares it used to be published under. A package's `(type
|
|
|
491
492
|
|
|
492
493
|
The published content index — the artifact packages exchange addresses through. **A package publishes its own index; a consumer fetches the ones it depends on.** That is the whole mechanism, and it replaces a vendored link manifest that each repository committed a copy of every other repository's file into. Vendoring failed three ways, and only the last is about staleness:
|
|
493
494
|
|
|
494
|
-
| Export | Signature | Returns | Use it when
|
|
495
|
-
| ------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
496
|
-
| `METADATA_RELATIONSHIP_KINDS` | `const METADATA_RELATIONSHIP_KINDS` | — | The relationship kinds that are dependencies, and therefore citable.
|
|
497
|
-
| `metadataFileName` | `function metadataFileName(pkg)` | {string} The file name, e.g. | What a package's content index is called, wherever it is written or fetched.
|
|
498
|
-
| `metadataRelationships` | `function metadataRelationships(config)` | {Array<{id: string, manifest: string, kind: string, verified: string\|undefined}>} The dependencies, in declaration order. | Every dependency whose published index this build resolves addresses through.
|
|
499
|
-
| `
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
503
|
-
| `
|
|
504
|
-
| `
|
|
505
|
-
| `
|
|
506
|
-
| `
|
|
507
|
-
| `
|
|
508
|
-
| `
|
|
495
|
+
| Export | Signature | Returns | Use it when |
|
|
496
|
+
| ------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
497
|
+
| `METADATA_RELATIONSHIP_KINDS` | `const METADATA_RELATIONSHIP_KINDS` | — | The relationship kinds that are dependencies, and therefore citable. |
|
|
498
|
+
| `metadataFileName` | `function metadataFileName(pkg)` | {string} The file name, e.g. | What a package's content index is called, wherever it is written or fetched. |
|
|
499
|
+
| `metadataRelationships` | `function metadataRelationships(config)` | {Array<{id: string, manifest: string, kind: string, verified: string\|undefined}>} The dependencies, in declaration order. | Every dependency whose published index this build resolves addresses through. |
|
|
500
|
+
| `noContentIndexPackages` | `function noContentIndexPackages(config)` | {ReadonlySet<string>} The content package names. | Every package a relationship declares `contentIndex: false` on, keyed by the content package name a link into it would use. |
|
|
501
|
+
| `metadataCacheDir` | `function metadataCacheDir(config, id, version)` | {string} The directory. | The cache directory for one dependency's index at one version. |
|
|
502
|
+
| `isComplete` | `const isComplete` | {boolean} True when it was fetched to completion. | Whether a dependency's cache is present and complete. |
|
|
503
|
+
| `markComplete` | `function markComplete(dir)` | {void} | Mark a dependency's cache complete. |
|
|
504
|
+
| `cachedMetadataFiles` | `function cachedMetadataFiles(config)` | {string[]} One index file per declared dependency. | The fetched index files this build resolves foreign addresses against. |
|
|
505
|
+
| `cachedMetadataIndexes` | `function cachedMetadataIndexes(config)` | {Array<{id: string, file: string}>} One entry per declared dependency. | The same fetched indexes, each paired with the package that published it. |
|
|
506
|
+
| `newestVersionDir` | `function newestVersionDir(dirs)` | {string} The newest one. | The newest cached version among several version-keyed cache directories. |
|
|
507
|
+
| `loadForeignIndexes` | `function loadForeignIndexes(config, localPackages, bases` | {{index: Map<string, object>, packages: Set<string>, stale: Array<{package: string, reason: string}>}} The resolved addresses, which packages contributed, and what could not be read. | Resolve every foreign address this build can cite, from the fetched indexes. |
|
|
508
|
+
| `cachedIndexPath` | `function cachedIndexPath(config, pkg)` | {string} A path to name in a diagnostic. | Where a dependency's fetched index sits, for naming it in a diagnostic. |
|
|
509
|
+
| `unaddressableForeignPackages` | `function unaddressableForeignPackages(foreignIndex)` | {Array<{package: string, entries: number, sampleKey: string}>} One finding per drifted package, in the order the index first names each. | Whether a fetched index can still be _addressed_, as distinct from read. |
|
|
510
|
+
| `formatUnaddressableFinding` | `function formatUnaddressableFinding(finding, config)` | {string} The formatted diagnostic, path first on the line. | One finding, in the standard `file:line:column: severity: message` form. |
|
|
509
511
|
|
|
510
512
|
### `engine.foundryEntries`
|
|
511
513
|
|
|
@@ -621,8 +623,7 @@ Publishing a content tree as a website. Compiling a content tree into compendium
|
|
|
621
623
|
| `writeSectionLandings` | `function writeSectionLandings(outRoot,` | {number} How many landings were written. | Writes the Hugo sections a published tree declares. |
|
|
622
624
|
| `pluralTitle` | `function pluralTitle(name)` | {string} The display title. | A section landing's title, from its directory name — `macro` → `Macros`. |
|
|
623
625
|
| `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function, afterLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
|
|
624
|
-
| `
|
|
625
|
-
| `buildSite` | `buildSite({ config, outRoot, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
|
|
626
|
+
| `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
|
|
626
627
|
|
|
627
628
|
### `engine.contentLint`
|
|
628
629
|
|
|
@@ -835,14 +836,14 @@ Shared helpers for the pack compilers in `packages/content-build/`. The HeroicLa
|
|
|
835
836
|
|
|
836
837
|
Wikilink resolution for the pack compilers. Content notes link to one another with wikilinks rather than file paths:
|
|
837
838
|
|
|
838
|
-
| Export | Signature | Returns
|
|
839
|
-
| -------------------- | ----------------------------------------------------------------------- |
|
|
840
|
-
| `resolveItemDocType` | `function resolveItemDocType(qualifier, types)` | {string\|null} The underlying document type, or `null` when the qualifier is not a virtual one.
|
|
841
|
-
| `readQualifier` | `function readQualifier(target, types, packages)`
|
|
842
|
-
| `anchorPageId` | `function anchorPageId(noteId, anchorSlug)` | {string} A 16-character alphanumeric id.
|
|
843
|
-
| `buildWikilinkIndex` | `function buildWikilinkIndex(docs, packageId, foreign, contentPackage)` | {{byShortcode: Map<string, object>, types: Set<string>}} `types` is every type the tree actually contains, so a qualifier naming no real type can be told apart from a missing target.
|
|
844
|
-
| `resolveReference` | `function resolveReference(index, ref, hint)` | {{name?: string, uuid?: string, address?: string, subType?: string}\|undefined} The target, or `undefined` where nothing answers.
|
|
845
|
-
| `convertWikilinks` | `function convertWikilinks(markdown,` | {{markdown: string, unresolved: Array<{link: string, target: string, offset: number, reason: string, packages?: string[], anchor?: string}>}} Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all three resolvers share — `ambiguous` carries the claiming `packages` and `unknown-anchor` the section it named.
|
|
839
|
+
| Export | Signature | Returns | Use it when |
|
|
840
|
+
| -------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
841
|
+
| `resolveItemDocType` | `function resolveItemDocType(qualifier, types)` | {string\|null} The underlying document type, or `null` when the qualifier is not a virtual one. | Reads a qualifier as the **virtual `doc<type>`** form, or reports that it is not one. |
|
|
842
|
+
| `readQualifier` | `function readQualifier(target, types, packages, noIndexPackages)` | {{type: string, shortcode: string, itemDoc: boolean, package?: string, system?: string, reason?: undefined} \| {reason: "unknown-type"\|"no-content-index", package?: string} \| null} The resolved qualifier; a `reason` when the target is definitely qualified but names no known type or no fetched index; or `null` when it is not an address at all. | Read a link target as a **qualified** `type-shortcode` reference, or report that it does not parse as one. |
|
|
843
|
+
| `anchorPageId` | `function anchorPageId(noteId, anchorSlug)` | {string} A 16-character alphanumeric id. | The deterministic JournalEntryPage id for one anchor: SHA-256 of `"<noteId>-<anchorSlug>"`, base64-encoded, reduced to the 16 alphanumeric characters a Foundry id allows. |
|
|
844
|
+
| `buildWikilinkIndex` | `function buildWikilinkIndex(docs, packageId, foreign, contentPackage)` | {{byShortcode: Map<string, object>, types: Set<string>}} `types` is every type the tree actually contains, so a qualifier naming no real type can be told apart from a missing target. | Builds the link-resolution tables for a content tree. |
|
|
845
|
+
| `resolveReference` | `function resolveReference(index, ref, hint)` | {{name?: string, uuid?: string, address?: string, subType?: string}\|undefined} The target, or `undefined` where nothing answers. | Resolve one reference — a bare shortcode, a short address or a canonical one — to what a compendium can use. |
|
|
846
|
+
| `convertWikilinks` | `function convertWikilinks(markdown,` | {{markdown: string, unresolved: Array<{link: string, target: string, offset: number, reason: string, packages?: string[], anchor?: string}>}} Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all three resolvers share — `ambiguous` carries the claiming `packages` and `unknown-anchor` the section it named. | Rewrites every wikilink in a markdown body as a Foundry UUID enricher. |
|
|
846
847
|
|
|
847
848
|
### `engine.wikilinkSyntax`
|
|
848
849
|
|
|
@@ -1269,6 +1270,7 @@ const config = defineConfig({
|
|
|
1269
1270
|
| `SITE_MODES` | `const SITE_MODES` | — | reading the publishing modes `publish.site` may name, weakest first |
|
|
1270
1271
|
| `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
|
|
1271
1272
|
| `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether a resolved configuration publishes the pages its content tree compiles to — the one question the site build and the content index both need answered identically |
|
|
1273
|
+
| `DERIVED_HUGO_KEYS` | `const DERIVED_HUGO_KEYS` | — | reading which Hugo keys `site.hugo` may not declare because the site build generates them, each naming its source (declaring one is an error naming the key); a dotted key covers everything beneath it |
|
|
1272
1274
|
|
|
1273
1275
|
## `./config`
|
|
1274
1276
|
|
|
@@ -1282,11 +1284,12 @@ const config = loadPackageBuildConfig();
|
|
|
1282
1284
|
console.log(config.stageDir);
|
|
1283
1285
|
```
|
|
1284
1286
|
|
|
1285
|
-
| Export | Signature
|
|
1286
|
-
| --------------------------- |
|
|
1287
|
-
| `DERIVED_MANIFEST_KEYS` | `const DERIVED_MANIFEST_KEYS`
|
|
1288
|
-
| `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)`
|
|
1289
|
-
| `loadPackageBuildConfig` | `loadPackageBuildConfig()`
|
|
1287
|
+
| Export | Signature | Returns | Use it when |
|
|
1288
|
+
| --------------------------- | ----------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1289
|
+
| `DERIVED_MANIFEST_KEYS` | `const DERIVED_MANIFEST_KEYS` | — | reading which manifest keys a repository may not declare because the build derives them (declaring one is an error naming the key) |
|
|
1290
|
+
| `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)` | `Readonly<PackageBuildConfig>` | validating the `packageBuild:` section from an already-loaded shared configuration — the pure half, usable without touching disk |
|
|
1291
|
+
| `loadPackageBuildConfig` | `loadPackageBuildConfig()` | `Readonly<PackageBuildConfig>` | reading and validating the repository's resolved package-build configuration from disk, read fresh on each call rather than cached at import |
|
|
1292
|
+
| `checkHomepage` | `checkHomepage(homepage, contentPackage)` | `void` | validating a resolved `homepage` against `contentPackage` — called by `content-build site` before the generated `baseURL` is written, not by `resolvePackageBuildConfig` itself |
|
|
1290
1293
|
|
|
1291
1294
|
## `./prettier`
|
|
1292
1295
|
|