@heroiclands/package-build 22.0.2 → 22.1.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 +79 -0
- package/CONTENT.md +34 -18
- package/README.md +1 -1
- package/bin/content-build.mjs +40 -22
- package/bin/package-build.mjs +34 -0
- package/config.mjs +88 -3
- package/content-config.mjs +246 -12
- package/docs/api.md +10 -7
- package/docs/commands.md +122 -32
- package/docs/configuration.md +257 -62
- package/docs/getting-started.md +15 -4
- package/docs/project-setup.md +45 -20
- package/engine/content-index.mjs +25 -6
- package/engine/pack-config.mjs +21 -2
- package/engine/site-build.mjs +15 -52
- package/engine/site-config.mjs +462 -0
- package/engine/site-root.mjs +165 -0
- package/engine/svg-theme.mjs +140 -0
- package/manifest.mjs +12 -7
- package/package.json +2 -1
- package/stage.mjs +4 -3
- package/types/config.d.mts +80 -33
- package/types/content-config.d.mts +53 -0
- package/types/engine/site-build.d.mts +3 -25
- package/types/engine/site-config.d.mts +223 -0
- package/types/engine/site-root.d.mts +87 -0
- package/types/engine/svg-theme.d.mts +22 -0
- 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
|
/**
|
|
@@ -578,6 +585,13 @@ export function publishesContentPages(config) {
|
|
|
578
585
|
* Refused by a `documentation`
|
|
579
586
|
* package, which ships no Foundry
|
|
580
587
|
* package.
|
|
588
|
+
* @property {string} [homepage] `package.json`'s own `homepage` —
|
|
589
|
+
* the site build's `baseURL`. Checked
|
|
590
|
+
* by `checkHomepage` in
|
|
591
|
+
* `config.mjs`.
|
|
592
|
+
* @property {string|{name: string, email?: string, url?: string}} [author]
|
|
593
|
+
* `package.json`'s own `author`, in
|
|
594
|
+
* either of npm's forms.
|
|
581
595
|
* @property {PackageKind} packageKind Whether the package is a system, a
|
|
582
596
|
* module, or documentation — the kind
|
|
583
597
|
* that publishes a site and a book
|
|
@@ -637,6 +651,13 @@ export function publishesContentPages(config) {
|
|
|
637
651
|
* @property {string} contentPackage
|
|
638
652
|
* @property {string|null} foundryPackage `null` for a `documentation`
|
|
639
653
|
* package, which ships no Foundry package.
|
|
654
|
+
* @property {string|null} homepage `package.json`'s own `homepage`,
|
|
655
|
+
* checked by `checkHomepage` in
|
|
656
|
+
* `config.mjs`.
|
|
657
|
+
* @property {Readonly<{name: string, email?: string, url?: string}>|null} author
|
|
658
|
+
* `package.json`'s own `author`, normalised
|
|
659
|
+
* from either of npm's forms; `null` when
|
|
660
|
+
* the package declares none.
|
|
640
661
|
* @property {PackageKind} packageKind
|
|
641
662
|
* @property {string|null} assetRoot Derived, and **conditional**: the served
|
|
642
663
|
* Foundry asset root,
|
|
@@ -705,6 +726,8 @@ const CONFIG_KEYS = [
|
|
|
705
726
|
"rootDir",
|
|
706
727
|
"contentPackage",
|
|
707
728
|
"foundryPackage",
|
|
729
|
+
"homepage",
|
|
730
|
+
"author",
|
|
708
731
|
"packageKind",
|
|
709
732
|
"stats",
|
|
710
733
|
"itemBuilders",
|
|
@@ -726,7 +749,6 @@ const SYSTEM_KEYS = ["manifest", "compatibility"];
|
|
|
726
749
|
const COMPATIBILITY_KEYS = ["minimum", "verified"];
|
|
727
750
|
const DOCS_KEYS = ["itemFields"];
|
|
728
751
|
const SITE_KEYS = [
|
|
729
|
-
"out",
|
|
730
752
|
"base",
|
|
731
753
|
"assets",
|
|
732
754
|
"packages",
|
|
@@ -737,8 +759,14 @@ const SITE_KEYS = [
|
|
|
737
759
|
"pass",
|
|
738
760
|
"passOptions",
|
|
739
761
|
"backfillSections",
|
|
762
|
+
"list",
|
|
763
|
+
"notfound",
|
|
764
|
+
"hugo",
|
|
740
765
|
];
|
|
741
766
|
const SITE_TREE_KEYS = ["from", "section"];
|
|
767
|
+
const SITE_LIST_KEYS = ["shortcodes"];
|
|
768
|
+
const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
|
|
769
|
+
const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
|
|
742
770
|
const PDF_KEYS = ["title", "subtitle", "document", "out", "front", "fonts", "iconFonts", "binary"];
|
|
743
771
|
const PDF_FONT_KEYS = ["serif", "sans", "mono", "path"];
|
|
744
772
|
const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: "" });
|
|
@@ -753,6 +781,7 @@ const RELATIONSHIP_KEYS = [
|
|
|
753
781
|
"compatibility",
|
|
754
782
|
"itemCatalog",
|
|
755
783
|
];
|
|
784
|
+
const AUTHOR_KEYS = ["name", "email", "url"];
|
|
756
785
|
const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
|
|
757
786
|
const ITEM_REGISTRY_KEYS = ["system", "builders"];
|
|
758
787
|
const PACK_KEYS = [
|
|
@@ -964,6 +993,56 @@ function optionalString(value, field) {
|
|
|
964
993
|
return value;
|
|
965
994
|
}
|
|
966
995
|
|
|
996
|
+
/**
|
|
997
|
+
* npm's `author` field, in either of its two forms.
|
|
998
|
+
*
|
|
999
|
+
* `package.json` accepts a single string — `"Name <email> (url)"`, with the
|
|
1000
|
+
* email and the URL both optional — or an object carrying the same three
|
|
1001
|
+
* parts. Both normalise to one shape, so the site build reads one field
|
|
1002
|
+
* instead of branching on which form a repository happened to write.
|
|
1003
|
+
*
|
|
1004
|
+
* @type {RegExp}
|
|
1005
|
+
*/
|
|
1006
|
+
const AUTHOR_STRING = /^([^<(]*?)\s*(?:<([^>]*)>)?\s*(?:\(([^)]*)\))?\s*$/;
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* @param {unknown} value - The declared `author`, or `undefined`.
|
|
1010
|
+
* @returns {Readonly<{name: string, email?: string, url?: string}>|null}
|
|
1011
|
+
* `null` when the package declares none.
|
|
1012
|
+
*/
|
|
1013
|
+
function normalizeAuthor(value) {
|
|
1014
|
+
if (value === undefined) return null;
|
|
1015
|
+
if (typeof value === "string") {
|
|
1016
|
+
const match = AUTHOR_STRING.exec(value.trim());
|
|
1017
|
+
const name = match?.[1]?.trim();
|
|
1018
|
+
if (!match || !name) {
|
|
1019
|
+
fail(
|
|
1020
|
+
"author",
|
|
1021
|
+
'must be `"Name"`, `"Name <email>"`, `"Name (url)"` or ' +
|
|
1022
|
+
'`"Name <email> (url)"` — npm\'s own `author` forms',
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
return Object.freeze({
|
|
1026
|
+
name: /** @type {string} */ (name),
|
|
1027
|
+
...(match[2] ? { email: match[2] } : {}),
|
|
1028
|
+
...(match[3] ? { url: match[3] } : {}),
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
if (!isPlainObject(value)) {
|
|
1032
|
+
fail("author", "must be a string or an object with `name`, `email` and `url`");
|
|
1033
|
+
}
|
|
1034
|
+
const author = /** @type {Record<string, unknown>} */ (value);
|
|
1035
|
+
rejectUnknownKeys(author, AUTHOR_KEYS, "author.");
|
|
1036
|
+
const name = requireNonEmptyString(author.name, "author.name");
|
|
1037
|
+
return Object.freeze({
|
|
1038
|
+
name,
|
|
1039
|
+
...(author.email !== undefined ?
|
|
1040
|
+
{ email: optionalString(author.email, "author.email") }
|
|
1041
|
+
: {}),
|
|
1042
|
+
...(author.url !== undefined ? { url: optionalString(author.url, "author.url") } : {}),
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
967
1046
|
/**
|
|
968
1047
|
* @param {unknown} value
|
|
969
1048
|
* @param {string} where Field path used in error messages.
|
|
@@ -1504,21 +1583,149 @@ function normalizeSectionMap(value, where) {
|
|
|
1504
1583
|
return Object.freeze(out);
|
|
1505
1584
|
}
|
|
1506
1585
|
|
|
1586
|
+
/**
|
|
1587
|
+
* Hugo keys a repository may **not** declare under `site.hugo`, because the
|
|
1588
|
+
* site build generates them and would only overwrite what was written.
|
|
1589
|
+
*
|
|
1590
|
+
* The same rule `DERIVED_MANIFEST_KEYS` states for the manifest, for the same
|
|
1591
|
+
* reason: an authored `baseURL` would look authoritative, sit there unread,
|
|
1592
|
+
* and disagree with the site forever. Each key names where its value comes
|
|
1593
|
+
* from. A dotted key names a nested one, and covers everything beneath it —
|
|
1594
|
+
* `params.brand` refuses `params.brand.logo` too — so `site.hugo` reaches only
|
|
1595
|
+
* what the generator does not write.
|
|
1596
|
+
*
|
|
1597
|
+
* @type {Readonly<Record<string, string>>}
|
|
1598
|
+
*/
|
|
1599
|
+
export const DERIVED_HUGO_KEYS = Object.freeze({
|
|
1600
|
+
baseURL: "package.json `homepage`",
|
|
1601
|
+
title: "`packageBuild.manifest.title`",
|
|
1602
|
+
locale: "the organisation's locale, in `engine/site-config.mjs`",
|
|
1603
|
+
publishDir: "`contentPackage`, under the deployment root `build/site`",
|
|
1604
|
+
contentDir: "the fixed content mount, `build/hugo/content`",
|
|
1605
|
+
themesDir: "where `@heroiclands/hugo-theme` is installed",
|
|
1606
|
+
theme: "the installed `@heroiclands/hugo-theme`",
|
|
1607
|
+
disableKinds: "the toolchain, which renders the same kinds on every site",
|
|
1608
|
+
"params.description": "package.json `description`",
|
|
1609
|
+
"params.author": "package.json `author`",
|
|
1610
|
+
"params.cdnBaseURL": "`site.assets`",
|
|
1611
|
+
"params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
|
|
1612
|
+
"params.list": "`site.list`",
|
|
1613
|
+
"params.notfound": "`site.notfound`",
|
|
1614
|
+
"markup.goldmark.renderer.unsafe": "the toolchain, whose pages carry raw HTML",
|
|
1615
|
+
menu: "the navigation `content-build deps fetch` caches from heroiclands.org",
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* The `site.list` block — how a listing page renders.
|
|
1620
|
+
*
|
|
1621
|
+
* @param {unknown} value - The block, or `undefined`.
|
|
1622
|
+
* @returns {Readonly<{shortcodes: boolean}>} It, frozen, with every default filled.
|
|
1623
|
+
*/
|
|
1624
|
+
function normalizeSiteList(value) {
|
|
1625
|
+
if (value === undefined) return Object.freeze({ shortcodes: false });
|
|
1626
|
+
if (!isPlainObject(value)) fail("site.list", "must be a mapping");
|
|
1627
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1628
|
+
rejectUnknownKeys(input, SITE_LIST_KEYS, "site.list.");
|
|
1629
|
+
return Object.freeze({
|
|
1630
|
+
shortcodes: optionalBoolean(input.shortcodes, "site.list.shortcodes", false),
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* The `site.notfound` block — the wording of the "page not found" page.
|
|
1636
|
+
*
|
|
1637
|
+
* The theme renders the page for every site; what a repository supplies is
|
|
1638
|
+
* the tagline, the noun the body prose calls the site, and the routes back.
|
|
1639
|
+
* `tagline` and `sitenoun` are required once the block is present: a block
|
|
1640
|
+
* declaring only links would render the theme's generic wording above a list
|
|
1641
|
+
* of this site's routes, which reads as two sites.
|
|
1642
|
+
*
|
|
1643
|
+
* @param {unknown} value - The block, or `undefined`.
|
|
1644
|
+
* @returns {Readonly<object>|null} It, frozen; `null` when absent.
|
|
1645
|
+
*/
|
|
1646
|
+
function normalizeSiteNotfound(value) {
|
|
1647
|
+
if (value === undefined) return null;
|
|
1648
|
+
if (!isPlainObject(value)) fail("site.notfound", "must be a mapping");
|
|
1649
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1650
|
+
rejectUnknownKeys(input, SITE_NOTFOUND_KEYS, "site.notfound.");
|
|
1651
|
+
const out = {
|
|
1652
|
+
tagline: requireNonEmptyString(input.tagline, "site.notfound.tagline"),
|
|
1653
|
+
sitenoun: requireNonEmptyString(input.sitenoun, "site.notfound.sitenoun"),
|
|
1654
|
+
};
|
|
1655
|
+
if (input.heroimage !== undefined) {
|
|
1656
|
+
out.heroimage = requireNonEmptyString(input.heroimage, "site.notfound.heroimage");
|
|
1657
|
+
}
|
|
1658
|
+
if (input.links !== undefined) {
|
|
1659
|
+
if (!Array.isArray(input.links)) fail("site.notfound.links", "must be a list");
|
|
1660
|
+
out.links = Object.freeze(
|
|
1661
|
+
input.links.map((link, i) => {
|
|
1662
|
+
const where = `site.notfound.links[${i}]`;
|
|
1663
|
+
if (!isPlainObject(link)) fail(where, "must be a mapping");
|
|
1664
|
+
const entry = /** @type {Record<string, unknown>} */ (link);
|
|
1665
|
+
rejectUnknownKeys(entry, SITE_NOTFOUND_LINK_KEYS, `${where}.`);
|
|
1666
|
+
return Object.freeze({
|
|
1667
|
+
title: requireNonEmptyString(entry.title, `${where}.title`),
|
|
1668
|
+
url: requireNonEmptyString(entry.url, `${where}.url`),
|
|
1669
|
+
text: requireNonEmptyString(entry.text, `${where}.text`),
|
|
1670
|
+
});
|
|
1671
|
+
}),
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return Object.freeze(out);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* The `site.hugo` block — a mapping deep-merged over the generated Hugo
|
|
1679
|
+
* configuration, last.
|
|
1680
|
+
*
|
|
1681
|
+
* The escape hatch for the key nobody anticipated. Every key the generator
|
|
1682
|
+
* writes is refused here by {@link DERIVED_HUGO_KEYS}, naming its source, so
|
|
1683
|
+
* the block cannot grow into a second configuration file.
|
|
1684
|
+
*
|
|
1685
|
+
* @param {unknown} value - The block, or `undefined`.
|
|
1686
|
+
* @returns {Readonly<Record<string, unknown>>} It, frozen; `{}` when absent.
|
|
1687
|
+
*/
|
|
1688
|
+
function normalizeSiteHugo(value) {
|
|
1689
|
+
if (value === undefined) return Object.freeze({});
|
|
1690
|
+
if (!isPlainObject(value)) fail("site.hugo", "must be a mapping");
|
|
1691
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1692
|
+
for (const [dotted, source] of Object.entries(DERIVED_HUGO_KEYS)) {
|
|
1693
|
+
/** @type {unknown} */
|
|
1694
|
+
let at = input;
|
|
1695
|
+
for (const part of dotted.split(".")) {
|
|
1696
|
+
at = isPlainObject(at) ? /** @type {Record<string, unknown>} */ (at)[part] : undefined;
|
|
1697
|
+
if (at === undefined) break;
|
|
1698
|
+
}
|
|
1699
|
+
if (at !== undefined) {
|
|
1700
|
+
fail(
|
|
1701
|
+
`site.hugo.${dotted}`,
|
|
1702
|
+
`is derived from ${source} and must not be declared — it ` +
|
|
1703
|
+
`would be overwritten, and the two would disagree with ` +
|
|
1704
|
+
`nothing to say so`,
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return Object.freeze(structuredClone(input));
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1507
1711
|
/**
|
|
1508
1712
|
* The `site` section — how this repository frames the website it publishes.
|
|
1509
1713
|
*
|
|
1510
|
-
* Everything here is *framing*:
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1714
|
+
* Everything here is *framing*: what a section is called, which extra trees
|
|
1715
|
+
* are published beside the content, which named pass bundle supplies the
|
|
1716
|
+
* repository's own body rewrites, and the residue of the generated Hugo
|
|
1717
|
+
* configuration that is genuinely this repository's own. Where the Hugo tree
|
|
1718
|
+
* is written is not a choice: `content-build site` writes it under
|
|
1719
|
+
* `build/hugo/`, and a `site.out` is refused by name. How a page gets its
|
|
1720
|
+
* **address** is deliberately not here either — that is `publish.address`,
|
|
1721
|
+
* shared with the link manifest so the two cannot disagree about where a
|
|
1722
|
+
* page is.
|
|
1515
1723
|
*
|
|
1516
1724
|
* @param {unknown} value - The `site` block, or `undefined`.
|
|
1517
1725
|
* @returns {Readonly<object>} It, frozen, with every default filled.
|
|
1518
1726
|
*/
|
|
1519
1727
|
function normalizeSite(value) {
|
|
1520
1728
|
const empty = Object.freeze({
|
|
1521
|
-
out: "",
|
|
1522
1729
|
base: "",
|
|
1523
1730
|
assets: "",
|
|
1524
1731
|
packages: Object.freeze([]),
|
|
@@ -1529,10 +1736,23 @@ function normalizeSite(value) {
|
|
|
1529
1736
|
pass: "",
|
|
1530
1737
|
passOptions: Object.freeze({}),
|
|
1531
1738
|
backfillSections: false,
|
|
1739
|
+
list: Object.freeze({ shortcodes: false }),
|
|
1740
|
+
notfound: null,
|
|
1741
|
+
hugo: Object.freeze({}),
|
|
1532
1742
|
});
|
|
1533
1743
|
if (value === undefined) return empty;
|
|
1534
1744
|
if (!isPlainObject(value)) fail("site", "must be a mapping");
|
|
1535
1745
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1746
|
+
// Refused by name, ahead of the vocabulary check: the useful thing to say
|
|
1747
|
+
// is not "no such key" but that the location is fixed.
|
|
1748
|
+
if (input.out !== undefined) {
|
|
1749
|
+
fail(
|
|
1750
|
+
"site.out",
|
|
1751
|
+
"is retired — the site build writes its content mount at " +
|
|
1752
|
+
"`build/hugo/content`, beside the generated `hugo.toml`, and " +
|
|
1753
|
+
"the location is not configurable. Remove the key",
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1536
1756
|
rejectUnknownKeys(input, SITE_KEYS, "site.");
|
|
1537
1757
|
|
|
1538
1758
|
const trees = [];
|
|
@@ -1574,7 +1794,6 @@ function normalizeSite(value) {
|
|
|
1574
1794
|
}
|
|
1575
1795
|
|
|
1576
1796
|
return Object.freeze({
|
|
1577
|
-
out: input.out === undefined ? "" : requireNonEmptyString(input.out, "site.out"),
|
|
1578
1797
|
base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
|
|
1579
1798
|
assets: normalizeSiteAssets(input.assets),
|
|
1580
1799
|
packages: Object.freeze(packages),
|
|
@@ -1588,6 +1807,9 @@ function normalizeSite(value) {
|
|
|
1588
1807
|
Object.freeze({})
|
|
1589
1808
|
: Object.freeze({ ...input.passOptions }),
|
|
1590
1809
|
backfillSections: optionalBoolean(input.backfillSections, "site.backfillSections", false),
|
|
1810
|
+
list: normalizeSiteList(input.list),
|
|
1811
|
+
notfound: normalizeSiteNotfound(input.notfound),
|
|
1812
|
+
hugo: normalizeSiteHugo(input.hugo),
|
|
1591
1813
|
});
|
|
1592
1814
|
}
|
|
1593
1815
|
|
|
@@ -2328,10 +2550,17 @@ export function defineConfig(config) {
|
|
|
2328
2550
|
}
|
|
2329
2551
|
}
|
|
2330
2552
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2553
|
+
// A Foundry package need not compile anything. A module may ship assets and
|
|
2554
|
+
// nothing else — alternative art for another package is the case, and it is
|
|
2555
|
+
// an ordinary module that installs, is enabled, and supplies files. So
|
|
2556
|
+
// `packs: []` is a package saying it compiles no documents, not a package
|
|
2557
|
+
// that forgot to say which.
|
|
2558
|
+
//
|
|
2559
|
+
// What that leaves uncovered is a tree of notes with no pack to compile them
|
|
2560
|
+
// into, which would be silently ignored. `packs` cannot see the tree, so the
|
|
2561
|
+
// walk reports it: a declared pack that compiles nothing from a non-empty
|
|
2562
|
+
// tree already fails, and so does a note whose `pack:` names none.
|
|
2563
|
+
if (!documentation && !Array.isArray(input.packs)) fail("packs", "must be an array");
|
|
2335
2564
|
const declaredPacks = Array.isArray(input.packs) ? input.packs : [];
|
|
2336
2565
|
const packs = declaredPacks.map((pack, index) => normalizePack(pack, `packs[${index}]`));
|
|
2337
2566
|
|
|
@@ -2486,6 +2715,11 @@ export function defineConfig(config) {
|
|
|
2486
2715
|
rootDir,
|
|
2487
2716
|
contentPackage: requireContentPackage(input.contentPackage, docEntryTypes),
|
|
2488
2717
|
foundryPackage,
|
|
2718
|
+
// `package.json`'s own address and byline. `homepage` is checked by
|
|
2719
|
+
// `checkHomepage` in `config.mjs`.
|
|
2720
|
+
homepage:
|
|
2721
|
+
input.homepage === undefined ? null : requireNonEmptyString(input.homepage, "homepage"),
|
|
2722
|
+
author: normalizeAuthor(input.author),
|
|
2489
2723
|
packageKind: /** @type {PackageKind} */ (packageKind),
|
|
2490
2724
|
// Foundry serves a package's files from `<kind>/<id>/`, so this is the
|
|
2491
2725
|
// one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
|
package/docs/api.md
CHANGED
|
@@ -621,8 +621,7 @@ Publishing a content tree as a website. Compiling a content tree into compendium
|
|
|
621
621
|
| `writeSectionLandings` | `function writeSectionLandings(outRoot,` | {number} How many landings were written. | Writes the Hugo sections a published tree declares. |
|
|
622
622
|
| `pluralTitle` | `function pluralTitle(name)` | {string} The display title. | A section landing's title, from its directory name — `macro` → `Macros`. |
|
|
623
623
|
| `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. |
|
|
624
|
+
| `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
625
|
|
|
627
626
|
### `engine.contentLint`
|
|
628
627
|
|
|
@@ -1269,6 +1268,7 @@ const config = defineConfig({
|
|
|
1269
1268
|
| `SITE_MODES` | `const SITE_MODES` | — | reading the publishing modes `publish.site` may name, weakest first |
|
|
1270
1269
|
| `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
1270
|
| `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 |
|
|
1271
|
+
| `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
1272
|
|
|
1273
1273
|
## `./config`
|
|
1274
1274
|
|
|
@@ -1282,11 +1282,12 @@ const config = loadPackageBuildConfig();
|
|
|
1282
1282
|
console.log(config.stageDir);
|
|
1283
1283
|
```
|
|
1284
1284
|
|
|
1285
|
-
| Export | Signature
|
|
1286
|
-
| --------------------------- |
|
|
1287
|
-
| `DERIVED_MANIFEST_KEYS` | `const DERIVED_MANIFEST_KEYS`
|
|
1288
|
-
| `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)`
|
|
1289
|
-
| `loadPackageBuildConfig` | `loadPackageBuildConfig()`
|
|
1285
|
+
| Export | Signature | Returns | Use it when |
|
|
1286
|
+
| --------------------------- | ----------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1287
|
+
| `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) |
|
|
1288
|
+
| `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)` | `Readonly<PackageBuildConfig>` | validating the `packageBuild:` section from an already-loaded shared configuration — the pure half, usable without touching disk |
|
|
1289
|
+
| `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 |
|
|
1290
|
+
| `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
1291
|
|
|
1291
1292
|
## `./prettier`
|
|
1292
1293
|
|
|
@@ -1488,6 +1489,8 @@ console.log(normalizeRepoUrl("git@github.com:HeroicLands/sohl.git"));
|
|
|
1488
1489
|
| ------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1489
1490
|
| `ARTIFACTS` | `const ARTIFACTS` | — | reading the two Foundry package kinds (`"system"`, `"module"`) as the artifact name their manifest and release archive are called |
|
|
1490
1491
|
| `normalizeRepoUrl` | `normalizeRepoUrl(repository)` | `string` — normalised `https://` URL, no trailing slash | normalising `package.json`'s `repository` field (object or shorthand string, `git+…git` or plain) to the exact URL Foundry fetches release assets from |
|
|
1492
|
+
| `BUILT_IN_ASSET_TRANSFORMS` | `const BUILT_IN_ASSET_TRANSFORMS` | `Readonly<Record<string, string>>` | the asset transforms this package ships, by the name `packageBuild.assetTransform` writes — `svg-theme` themes an icon to the reader's colour scheme |
|
|
1493
|
+
| `resolveAssetTransform` | `resolveAssetTransform(declared, rootDir)` | `string` — an absolute path to import | resolving a declared transform: a built-in name reaches the shipped module, anything else is a path against the repository root |
|
|
1491
1494
|
| `HOMEPAGE_ORIGIN` | `HOMEPAGE_ORIGIN` | `string` — the origin every package's homepage is served from | stating the site origin once, so the address a manifest advertises and the address the configuration documents cannot disagree |
|
|
1492
1495
|
| `packageHomepage` | `packageHomepage(contentPackage)` | `string` — `<origin>/<contentPackage>/` | deriving the homepage a manifest's `url` points at; throws when given no name rather than advertising `<origin>/undefined/` |
|
|
1493
1496
|
| `releaseUrls` | `releaseUrls({ repoUrl, homeUrl, version, artifact })` | `{url: string, bugs: string, manifest: string, download: string}` | building the manifest's addresses — `url` is the homepage a reader follows before installing, while `bugs`, `manifest` and `download` stay on the repository holding the artefacts |
|
package/docs/commands.md
CHANGED
|
@@ -260,6 +260,60 @@ package-build: `packageKind: documentation` ships no Foundry package, so there i
|
|
|
260
260
|
`content-build site`, `content-build pdf`, `package-build bundle check`,
|
|
261
261
|
[Configuration](configuration.md).
|
|
262
262
|
|
|
263
|
+
### `package-build site-root`
|
|
264
|
+
|
|
265
|
+
**NAME**
|
|
266
|
+
|
|
267
|
+
`package-build site-root` — write the deployment's `_headers` and `_redirects`.
|
|
268
|
+
|
|
269
|
+
**SYNOPSIS**
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
package-build site-root [--out <dir>]
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**DESCRIPTION**
|
|
276
|
+
|
|
277
|
+
Hugo renders into `<out>/<contentPackage>/`, because the deployment carries the
|
|
278
|
+
`/<contentPackage>/` prefix physically and the routing layer is a
|
|
279
|
+
path-preserving pass-through. The directory that is _uploaded_ is its parent,
|
|
280
|
+
and Cloudflare Pages reads `_headers` and `_redirects` from there and nowhere
|
|
281
|
+
else — a copy inside the prefix is published as a text file and never applied.
|
|
282
|
+
Hugo owns everything under the prefix; this owns what sits beside it.
|
|
283
|
+
|
|
284
|
+
Two things are written. Indexing is suppressed on every address a deployment
|
|
285
|
+
answers on but nobody advertises — the project's `pages.dev`, the per-deployment
|
|
286
|
+
`pages.dev`, and the custom domain the routing layer fetches — each of which
|
|
287
|
+
would otherwise compete with the canonical URL in search results. And both
|
|
288
|
+
spellings of the prefix root redirect to the landing, with a lifetime pinned on
|
|
289
|
+
the 301, because Pages sets no `Cache-Control` on a redirect it generates and a
|
|
290
|
+
301 without one is cached indefinitely on the most-linked URL there is.
|
|
291
|
+
|
|
292
|
+
The rules are scoped to those hostnames, so a site deployed under a domain of
|
|
293
|
+
its own stays indexable.
|
|
294
|
+
|
|
295
|
+
**OPTIONS**
|
|
296
|
+
|
|
297
|
+
`--out <dir>` — the directory that is deployed. Defaults to `build/site`.
|
|
298
|
+
|
|
299
|
+
**EXIT STATUS**
|
|
300
|
+
|
|
301
|
+
1 when `<out>/<contentPackage>/` holds no rendered site, which means the site
|
|
302
|
+
build has not run and writing root files would publish a deployment with nothing
|
|
303
|
+
under the prefix. Otherwise 0.
|
|
304
|
+
|
|
305
|
+
**EXAMPLES**
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
$ package-build site-root
|
|
309
|
+
✅ Wrote build/site/_headers.
|
|
310
|
+
✅ Wrote build/site/_redirects.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**SEE ALSO**
|
|
314
|
+
|
|
315
|
+
`content-build site`, [Configuration](configuration.md).
|
|
316
|
+
|
|
263
317
|
`package-build lang <action>` asks three independent questions about this
|
|
264
318
|
repository's localization, each blind to what the others see: `check`,
|
|
265
319
|
`coverage` and `hardcoded`, one section below per action.
|
|
@@ -933,17 +987,28 @@ content-build deps fetch [--from <zip|dir>] [--id <id>]
|
|
|
933
987
|
|
|
934
988
|
**DESCRIPTION**
|
|
935
989
|
|
|
936
|
-
The only action is `fetch`, which fills
|
|
937
|
-
declared dependency,
|
|
938
|
-
declare `itemCatalog: true
|
|
939
|
-
`
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
990
|
+
The only action is `fetch`, which fills three caches under `build/cache/`:
|
|
991
|
+
the **content index** of every declared dependency, the **item catalogue**
|
|
992
|
+
of those that additionally declare `itemCatalog: true`, and the **site
|
|
993
|
+
navigation** — `https://www.heroiclands.org/nav.json`, the header menu
|
|
994
|
+
every package site renders, fetched for every package because every package
|
|
995
|
+
publishes a site. Its own command rather than a step of `package compile`
|
|
996
|
+
or `site`, so neither reaches the network — a build that downloads silently
|
|
997
|
+
is not reproducible and hides a dependency's version change behind a
|
|
998
|
+
passing run. Each cache is stamped complete only once its fetch finishes,
|
|
999
|
+
so a half-finished one reads as cold.
|
|
1000
|
+
|
|
1001
|
+
The navigation is fetched first, because every package needs it and it
|
|
1002
|
+
depends on nothing a repository declares — so a dependency whose release
|
|
1003
|
+
cannot be read stops the run with the navigation already cached.
|
|
1004
|
+
|
|
1005
|
+
`--from` fills the cache from a locally built artifact — a package zip, or
|
|
1006
|
+
the directory it was built from — instead of a release, which is what makes
|
|
1007
|
+
testing a dependency change against its consumers possible before any of it
|
|
1008
|
+
ships; `--id` names which declared dependency `--from` supplies, needed only
|
|
1009
|
+
when the repository declares more than one. `--from` fills that one
|
|
1010
|
+
dependency's caches and nothing else: the navigation is not an artifact of
|
|
1011
|
+
any dependency, and is fetched by a plain `deps fetch`.
|
|
947
1012
|
|
|
948
1013
|
**OPTIONS**
|
|
949
1014
|
|
|
@@ -962,6 +1027,7 @@ including when the repository declares no dependencies at all.
|
|
|
962
1027
|
|
|
963
1028
|
```
|
|
964
1029
|
$ content-build deps fetch
|
|
1030
|
+
[…] Fetched the site navigation to build/cache/navigation/nav.json.
|
|
965
1031
|
[…] No relationship declares `itemCatalog: true`; nothing to fetch.
|
|
966
1032
|
[…] This package declares no dependencies.
|
|
967
1033
|
|
|
@@ -971,7 +1037,7 @@ $ content-build deps fetch --from build/dist/module.zip
|
|
|
971
1037
|
|
|
972
1038
|
**SEE ALSO**
|
|
973
1039
|
|
|
974
|
-
`content-build addresses diff`, [Configuration](configuration.md).
|
|
1040
|
+
`content-build site`, `content-build addresses diff`, [Configuration](configuration.md).
|
|
975
1041
|
|
|
976
1042
|
### `content-build docs item-fields`
|
|
977
1043
|
|
|
@@ -1461,54 +1527,78 @@ $ content-build content-index
|
|
|
1461
1527
|
|
|
1462
1528
|
**NAME**
|
|
1463
1529
|
|
|
1464
|
-
|
|
1530
|
+
Build the Hugo source tree from the content tree.
|
|
1465
1531
|
|
|
1466
1532
|
**SYNOPSIS**
|
|
1467
1533
|
|
|
1468
1534
|
```
|
|
1469
|
-
content-build site
|
|
1535
|
+
content-build site
|
|
1470
1536
|
```
|
|
1471
1537
|
|
|
1472
1538
|
**DESCRIPTION**
|
|
1473
1539
|
|
|
1474
|
-
|
|
1540
|
+
Writes the whole Hugo source tree under `build/hugo/` — the sibling of
|
|
1475
1541
|
`package compile`: the same tree, rendered as pages instead of compiled
|
|
1476
1542
|
into packs. Everything a consumer would otherwise write for itself happens
|
|
1477
1543
|
here: the walk, address derivation, the address index, table expansion,
|
|
1478
|
-
wikilink resolution, code-fence protection, the foreign-manifest merge
|
|
1479
|
-
|
|
1480
|
-
|
|
1544
|
+
wikilink resolution, code-fence protection, the foreign-manifest merge, the
|
|
1545
|
+
section-landing backfill, and the Hugo configuration itself. The consumer's
|
|
1546
|
+
script then runs Hugo over the tree — `hugo --source build/hugo` — and this
|
|
1547
|
+
command never does.
|
|
1548
|
+
|
|
1549
|
+
Three things are written, and nothing outside `build/`:
|
|
1550
|
+
|
|
1551
|
+
- `build/hugo/hugo.toml`, generated on every run from `package.json`
|
|
1552
|
+
(`homepage`, `description`, `author`), `package-build.config.yaml`
|
|
1553
|
+
(`packageBuild.manifest.title`, `site.assets`, `site.list`,
|
|
1554
|
+
`site.notfound`, `site.hugo`), the organisation's constants, the installed
|
|
1555
|
+
`@heroiclands/hugo-theme`'s location, and the navigation `deps fetch`
|
|
1556
|
+
cached. Every value's source is listed under
|
|
1557
|
+
[the generated Hugo configuration](configuration.md#the-generated-hugo-configuration).
|
|
1558
|
+
- `build/hugo/content/`, the content mount — the homepage at its root, and
|
|
1559
|
+
the content tree's pages below `publish.address.prefix`. Wiped on every
|
|
1560
|
+
run.
|
|
1561
|
+
- `publishDir` pointing Hugo at `build/site/<contentPackage>/`, the
|
|
1562
|
+
deployment root `package-build site-root` writes beside. Nothing Hugo
|
|
1563
|
+
reads lands in what is published.
|
|
1564
|
+
|
|
1565
|
+
The configuration's sources are read before the output tree is touched, so
|
|
1566
|
+
a missing `homepage`, a cold navigation cache or an uninstalled theme fails
|
|
1567
|
+
with the previous site intact. Every gate is then checked and reported, and
|
|
1568
|
+
the run stops at the first that fires, ordered so the report names the cause
|
|
1481
1569
|
rather than its symptoms — an unusable dependency manifest, reported after
|
|
1482
1570
|
the links that failed because of it, would otherwise read as a pile of
|
|
1483
|
-
broken notes. Reads the content tree named by `paths.content
|
|
1484
|
-
`--out`, defaulting to the configured `site.out`, which is wiped on every
|
|
1485
|
-
run.
|
|
1571
|
+
broken notes. Reads the content tree named by `paths.content`.
|
|
1486
1572
|
|
|
1487
1573
|
**OPTIONS**
|
|
1488
1574
|
|
|
1489
|
-
|
|
1490
|
-
| ------- | ------ | ------------------------- | -------------------------------------------------------- |
|
|
1491
|
-
| `--out` | string | the configured `site.out` | Write the mount here instead of the configured location. |
|
|
1575
|
+
None.
|
|
1492
1576
|
|
|
1493
1577
|
**EXIT STATUS**
|
|
1494
1578
|
|
|
1495
|
-
1 if `
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1579
|
+
1 if `package.json` declares no `homepage`, or one that does not end
|
|
1580
|
+
`/<contentPackage>/`; if `packageBuild.manifest.title` is undeclared; if the
|
|
1581
|
+
navigation has not been fetched (`content-build deps fetch` fills the cache
|
|
1582
|
+
and is named in the message); or if `@heroiclands/hugo-theme` is not
|
|
1583
|
+
installed. 1 if any gate fires — no homepage or two competing for it, a
|
|
1584
|
+
frontmatter wikilink, an address that cannot be derived, a stale or
|
|
1585
|
+
unaddressable dependency manifest, an address published twice, a table that
|
|
1586
|
+
failed to expand, or a dead wikilink. Otherwise 0.
|
|
1501
1587
|
|
|
1502
1588
|
**EXAMPLES**
|
|
1503
1589
|
|
|
1504
1590
|
```
|
|
1505
1591
|
$ content-build site
|
|
1506
|
-
[…] wrote 1 homepage(s) + 1 content page(s) + 0 tree page(s) + 0 landing(s) to build/
|
|
1592
|
+
[…] wrote 1 homepage(s) + 1 content page(s) + 0 tree page(s) + 0 landing(s) to build/hugo/content
|
|
1593
|
+
[…] wrote build/hugo/hugo.toml
|
|
1594
|
+
|
|
1595
|
+
$ content-build site
|
|
1596
|
+
[…] ERROR: the site navigation has not been fetched. Run `content-build deps fetch` first.
|
|
1507
1597
|
```
|
|
1508
1598
|
|
|
1509
1599
|
**SEE ALSO**
|
|
1510
1600
|
|
|
1511
|
-
`content-build package <action> [pack] [entry]`, `content-build pdf`, `content-build content-index [root]`,
|
|
1601
|
+
`content-build deps fetch`, `package-build site-root`, `content-build package <action> [pack] [entry]`, `content-build pdf`, `content-build content-index [root]`,
|
|
1512
1602
|
[Diagnostics](diagnostics.md), [Configuration](configuration.md).
|
|
1513
1603
|
|
|
1514
1604
|
### `content-build pdf`
|