@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 CHANGED
@@ -1,5 +1,71 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f02b25a: **A relationship may declare `contentIndex: false`.** A `requires` or
8
+ `systems` entry naming a package Foundry installs but the content tree never
9
+ cites by wikilink no longer needs a fetched index just to satisfy
10
+ `deps fetch`. `contentIndex` (default `true`) narrows a relationship declaring
11
+ it `false` to the Foundry manifest only: `deps fetch` fetches nothing for it,
12
+ and a wikilink into it fails at the link, naming the key, rather than
13
+ resolving against a stale declaration or an index nobody fetched. It cannot be
14
+ combined with `itemCatalog: true`, which extracts items from the same index
15
+ this declares there is none of.
16
+ - 07bf15d: **The generated Hugo configuration emits tag pages for a site whose notes carry `tags:`.** `content-build site` reads whether any note in the tree carries `tags:` and, when at least one does, writes `[taxonomies] tag = "tags"` and `[outputs] taxonomy = ["HTML"], term = ["HTML"]` into `build/hugo/hugo.toml`, and leaves `taxonomy` and `term` enabled among `disableKinds`. A site whose notes carry no `tags:` gets the same configuration as before — `taxonomy`, `term` and `RSS` all disabled, no `[taxonomies]` or `[outputs]` block. `site.hugo.disableKinds`, `.taxonomies` and `.outputs` stay refused under `site.hugo`, each naming this derivation as the source.
17
+
18
+ ## 22.1.0
19
+
20
+ ### Minor Changes
21
+
22
+ - b0ec919: **The site build generates the Hugo configuration.** `content-build site`
23
+ writes the whole Hugo source tree under `build/hugo/` — a generated
24
+ `hugo.toml`, the content mount at `build/hugo/content/`, Hugo's cache —
25
+ beside the deployment root `build/site/`, and the repository carries no Hugo
26
+ configuration of its own. Every value in the generated file has one source:
27
+ `package.json` (`homepage`, `description`, `author`),
28
+ `package-build.config.yaml` (`packageBuild.manifest.title`, `site.assets`,
29
+ `site.list`, `site.notfound`), the installed `@heroiclands/hugo-theme`, and
30
+ the organisation's brand links and navigation. `package.json`'s `homepage`
31
+ is checked on every site build: it must be
32
+ `https://www.heroiclands.org/<contentPackage>/`.
33
+
34
+ **`deps fetch` also fetches the navigation.** The header menu every site
35
+ renders comes from `https://www.heroiclands.org/nav.json`, cached under
36
+ `build/cache/navigation/` by `content-build deps fetch` and read from the
37
+ cache by the site build — a cold cache fails naming `deps fetch`, so a
38
+ package with no other dependency now runs `deps fetch` before `site` too.
39
+
40
+ **To migrate a repository:**
41
+
42
+ - Delete `site/` and its `.gitignore` entries (`site/content`, `site/public`,
43
+ `site/resources`), and any `packageBuild.clean.extra` entry naming them.
44
+ - Delete `site.out` from `package-build.config.yaml`; the location is fixed.
45
+ - Move the `[params.notfound]` block from the old `hugo.toml` to
46
+ `site.notfound` (`tagline`, `sitenoun`, optional `heroimage` and
47
+ `links[]` of `{title, url, text}`), `params.list.shortcodes` to
48
+ `site.list.shortcodes`, and `params.cdnBaseURL` to `site.assets` where it
49
+ was not already there. Anything else the file carried that the generator
50
+ does not write — `markup.goldmark.extensions.linkify`, say — goes under
51
+ `site.hugo`, which is deep-merged last; every key the generator writes is
52
+ refused there, naming its source.
53
+ - Point the npm scripts at the generated tree:
54
+ `hugo --source build/hugo --minify --gc --cleanDestinationDir` and
55
+ `hugo server --source build/hugo`.
56
+ - Make sure `package.json` declares `homepage`, `description` and `author`,
57
+ and that `@heroiclands/hugo-theme` is under `devDependencies`.
58
+ - a5e83fd: **A manifest's `description` comes from `package.json`.**
59
+ `packageBuild.manifest.description` is refused, the same way `id` and
60
+ `version` are — delete it; the manifest carries `package.json`'s own
61
+ `description`.
62
+
63
+ **`package.json`'s `homepage` and `author` are read and normalised.**
64
+ `homepage` names the address every package's site is served at — an
65
+ absolute URL ending `/<contentPackage>/` — and `author` is accepted in
66
+ either of npm's forms, a string or `{name, email, url}`, and normalised to
67
+ the object form.
68
+
3
69
  ## 22.0.3
4
70
 
5
71
  ### Patch Changes
package/CONTENT.md CHANGED
@@ -234,8 +234,9 @@ publish:
234
234
  site: content
235
235
  address: { prefix: guide/ }
236
236
 
237
- site:
238
- out: site/content
237
+ packageBuild:
238
+ manifest:
239
+ title: The Toolkit
239
240
 
240
241
  pdf:
241
242
  title: The Toolkit
@@ -1781,6 +1782,28 @@ asked for — a build that downloads silently is not reproducible and fails
1781
1782
  strangely offline. The cache is keyed by version, so changing the pinned version
1782
1783
  is a miss rather than a silent overwrite.
1783
1784
 
1785
+ ### A relationship may be a Foundry dependency only
1786
+
1787
+ `requires` and `systems` install with Foundry whether or not the tree cites
1788
+ them, and a package may need the one without the other — thalornaaltart
1789
+ `requires` Thalorna so Foundry installs the base module, and its one homepage
1790
+ note links nowhere. Declare `contentIndex: false` on that entry to say so:
1791
+
1792
+ ```yaml
1793
+ relationships:
1794
+ requires:
1795
+ - id: thalorna
1796
+ type: module
1797
+ manifest: https://github.com/HeroicLands/thalorna/releases/latest/download/module.json
1798
+ contentIndex: false
1799
+ ```
1800
+
1801
+ `deps fetch` fetches nothing for it — no cache directory, nothing to go stale —
1802
+ and a wikilink into it fails at the link, naming `contentIndex`, rather than
1803
+ resolving against a stale declaration or an index nobody fetched. It cannot be
1804
+ combined with `itemCatalog: true`, which extracts items from the same index
1805
+ this declares there is none of.
1806
+
1784
1807
  ### `packagebuild` needs no declaration
1785
1808
 
1786
1809
  package-build ships a set of images of its own — section banners chiefly — and a
@@ -2260,22 +2283,22 @@ address anyone links to. A package publishes its content index regardless — th
2260
2283
  licensing constraint is against publishing _pages_, not against the artifact
2261
2284
  existing — but nothing may declare it as a dependency.
2262
2285
 
2263
- The homepage's file is written at the root of `site.out` — the package's own
2264
- site root, one level above the content mount, which is where
2265
- `publish.address.prefix` puts everything else — under the name its address gives
2266
- it, `homepage-root.md`. As with every other page, the front matter's `url`
2286
+ The homepage's file is written at the root of the content mount,
2287
+ `build/hugo/content/` — the package's own site root, one level above the
2288
+ content mount, which is where `publish.address.prefix` puts everything else —
2289
+ under the name its address gives it, `homepage-root.md`. As with every other page, the front matter's `url`
2267
2290
  decides where it publishes, and states it relative to the site root — `site.base`
2268
2291
  does not reach it.
2269
2292
 
2270
2293
  **What it does not do is decide addresses.** Those come from `publish.address`,
2271
2294
  the same setting the content index reads, so a page and its index record cannot
2272
2295
  disagree about where the page is. Everything under `site:` is _framing_ —
2273
- where the tree is written, what a section is called, which extra trees are
2274
- published beside the content:
2296
+ what a section is called, which extra trees are published beside the content,
2297
+ and the residue of the generated Hugo configuration that is this repository's
2298
+ own:
2275
2299
 
2276
2300
  ```yaml
2277
2301
  site:
2278
- out: kb/content # required; wiped on every run
2279
2302
  base: /sohl/ # default: /<contentPackage>/ — hrefs only, never a page's `url:`
2280
2303
  packages: [sohl, thalorna] # default: just contentPackage
2281
2304
  backfillSections: true
@@ -2294,11 +2317,16 @@ site:
2294
2317
  description: Folk, animals and the things that walk the world.
2295
2318
  readmeSections:
2296
2319
  dev-docs: { title: Developer Documentation, banner: banners/dev-docs.webp }
2320
+ list: { shortcodes: true }
2321
+ notfound:
2322
+ tagline: Song of Heroic Lands has no page at
2323
+ sitenoun: site
2324
+ links:
2325
+ - { title: Knowledgebase, url: kb/, text: Every section, with the full content catalog. }
2297
2326
  ```
2298
2327
 
2299
2328
  | Key | What it decides |
2300
2329
  | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2301
- | `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below. |
2302
2330
  | `base` | Where the package is served: the prefix on every rendered `href`, and what a manifest `path` is measured against. It reaches no page's own `url:` — see [A page's URL is its address](#a-pages-url-is-its-address). Defaults to `/<contentPackage>/`. |
2303
2331
  | `packages` | Which content packages this site renders. Defaults to its own. |
2304
2332
  | `sections` | The Hugo sections this site declares, and what each says about itself — see below. |
@@ -2308,6 +2336,17 @@ site:
2308
2336
  | `trees` | Extra source trees published beside the content, preserving their source layout below a section. |
2309
2337
  | `pass` | A named bundle of this repository's own body rewrites. |
2310
2338
  | `passOptions` | That bundle's options. |
2339
+ | `assets` | The host every package's imagery is served from; the generated `params.cdnBaseURL`. |
2340
+ | `list` | How a listing page renders; the generated `params.list`. |
2341
+ | `notfound` | The wording of the "page not found" page; the generated `params.notfound`. |
2342
+ | `hugo` | A mapping deep-merged over the generated Hugo configuration, last. Every key the generator writes is refused here — see `docs/configuration.md`. |
2343
+
2344
+ Where the tree is written is not among them. `content-build site` writes the
2345
+ whole Hugo source tree under `build/hugo/` — the generated `hugo.toml`, the
2346
+ content mount at `build/hugo/content/`, Hugo's cache — and the consumer's
2347
+ script runs `hugo --source build/hugo` over it. The generated file's every
2348
+ value has a source the repository already states; `docs/configuration.md`
2349
+ lists them.
2311
2350
 
2312
2351
  ### What a section may declare
2313
2352
 
@@ -2409,15 +2448,14 @@ The **writers** name no keys: a section's `_index.md` is whatever the entry
2409
2448
  resolved to, `title` first. So extending the vocabulary is a change to the
2410
2449
  schema alone, and the two can no longer drift apart.
2411
2450
 
2412
- ### Why `out` is required
2451
+ ### Why the output location is fixed
2413
2452
 
2414
- The output tree is a build artifact and is **deleted on every run**, so that a
2415
- page whose note was renamed cannot linger and keep publishing. An unset `out`
2416
- resolves to the repository root, and the wipe then deletes the working tree.
2417
- That is not hypothetical it happened while this command was being written, on
2418
- a configuration that simply had no `site` section yet. So `out` is refused when
2419
- unset, and refused again when it resolves anywhere that is not strictly inside
2420
- the repository root.
2453
+ The content mount is a build artifact and is **deleted on every run**, so that a
2454
+ page whose note was renamed cannot linger and keep publishing. A configurable
2455
+ location is a location that can be unset — and an unset one resolves to the
2456
+ repository root, where the wipe deletes the working tree. A fixed one under
2457
+ `build/` can point nowhere else, so `site.out` is refused by name and the wipe
2458
+ needs no guard.
2421
2459
 
2422
2460
  ### Consumer passes are named, not imported
2423
2461
 
package/README.md CHANGED
@@ -95,7 +95,7 @@ packageBuild:
95
95
 
96
96
  clean:
97
97
  # Beyond the conventional build artifacts, which the library already knows.
98
- extra: [site/content, site/public, site/resources]
98
+ extra: [coverage]
99
99
 
100
100
  lang:
101
101
  sources: lang/*.json
@@ -79,6 +79,7 @@ import {
79
79
  unaddressableForeignPackages,
80
80
  formatUnaddressableFinding,
81
81
  } from "../engine/metadata-index.mjs";
82
+ import { fetchNavigation, generateHugoConfig, writeHugoConfig } from "../engine/site-config.mjs";
82
83
  import { renderItemFieldReference } from "../engine/field-reference.mjs";
83
84
  import { lintContentTree } from "../engine/content-lint.mjs";
84
85
  import { lintContentCharset } from "../engine/content-charset.mjs";
@@ -1484,7 +1485,17 @@ function pdfCommand() {
1484
1485
  * compiled into packs. Everything a consumer would otherwise write for itself —
1485
1486
  * the walk, the address derivation, the address index, table expansion,
1486
1487
  * wikilink resolution, code-fence protection, the foreign-manifest merge and
1487
- * the section-landing backfill — happens here, from configuration.
1488
+ * the section-landing backfill — happens here, from configuration. So does the
1489
+ * Hugo configuration: the whole source tree Hugo reads lands under
1490
+ * `build/hugo/`, and the consumer's script runs Hugo over it.
1491
+ *
1492
+ * **The Hugo configuration is generated before anything is written.** Its
1493
+ * sources — `package.json`'s `homepage`, the cached navigation, the installed
1494
+ * theme — are each a way the build can fail, and failing before the output
1495
+ * tree is cleared leaves the last good site in place to be looked at. It is
1496
+ * generated again once the site walk completes, because whether the site
1497
+ * emits taxonomy pages is read from the walk — whether any note carries
1498
+ * `tags:` — and that is not known until then.
1488
1499
  *
1489
1500
  * **Each gate is reported and the run stops at the first that fires.** They are
1490
1501
  * ordered so the report names the cause rather than its symptoms: an unusable
@@ -1497,20 +1508,24 @@ function pdfCommand() {
1497
1508
  function siteCommand() {
1498
1509
  return {
1499
1510
  command: "site",
1500
- describe: "Build a Hugo content tree from the content tree",
1501
- builder: (yargs) => {
1502
- yargs.option("out", {
1503
- describe: "Write the mount here instead of the configured `site.out`.",
1504
- type: "string",
1505
- });
1506
- },
1507
- handler: async (argv) => {
1511
+ describe: "Build the Hugo source tree from the content tree",
1512
+ builder: () => {},
1513
+ handler: async () => {
1508
1514
  try {
1515
+ const config = loadPackConfig();
1516
+ // Generated once before the walk, purely to fail fast on a
1517
+ // missing or mismatched source — `homepage`, the manifest
1518
+ // title, the cached navigation, the installed theme — while
1519
+ // the last good site is still in place to be looked at. Its
1520
+ // `disableKinds`/`taxonomies`/`outputs` are provisional: only
1521
+ // the site walk below knows whether any note carries `tags:`,
1522
+ // so the value actually written is regenerated after it runs.
1523
+ generateHugoConfig(config);
1509
1524
  const result = buildSite({
1510
- sqlTables: await prepareTreeSqlTables(loadPackConfig().paths.content, {
1511
- skipDirectories: loadPackConfig().skipDirectories,
1525
+ config,
1526
+ sqlTables: await prepareTreeSqlTables(config.paths.content, {
1527
+ skipDirectories: config.skipDirectories,
1512
1528
  }),
1513
- ...(argv.out ? { outRoot: argv.out } : {}),
1514
1529
  });
1515
1530
  const { gates } = result;
1516
1531
 
@@ -1623,6 +1638,9 @@ function siteCommand() {
1623
1638
  `${s.tree ?? 0} tree page(s) + ${s.landings} ` +
1624
1639
  `landing(s) to ${path.relative(process.cwd(), s.out)}`,
1625
1640
  );
1641
+ const hugo = generateHugoConfig(config, { hasTags: result.hasTags });
1642
+ const { file } = writeHugoConfig(config, hugo);
1643
+ log.info(`wrote ${path.relative(process.cwd(), file)}`);
1626
1644
  } catch (err) {
1627
1645
  reportFailure(err);
1628
1646
  process.exitCode = 1;
@@ -1782,16 +1800,19 @@ async function fetchFromLocalArtifact(config, argv) {
1782
1800
 
1783
1801
  /**
1784
1802
  * `deps fetch` — fill the caches this build resolves other packages through:
1785
- * the **content index** of every declared dependency, and the **item
1786
- * catalogue** of those additionally declaring `itemCatalog: true`.
1787
- *
1788
- * The two sets differ deliberately. Citing another package's *addresses* and
1789
- * embedding its *items* are separate edges, and a package may have either
1790
- * without the other `harn-ensemble` cites no foreign address and embeds
1791
- * 324,016 item references.
1792
- *
1793
- * Its own command rather than a step of `package compile`, so that a compile
1794
- * never reaches the network. A build that downloads silently is not
1803
+ * the **content index** of every declared dependency, the **item catalogue**
1804
+ * of those additionally declaring `itemCatalog: true`, and the **site
1805
+ * navigation** heroiclands.org publishes, which the site build writes its
1806
+ * menu from.
1807
+ *
1808
+ * The two dependency sets differ deliberately. Citing another package's
1809
+ * *addresses* and embedding its *items* are separate edges, and a package may
1810
+ * have either without the other — `harn-ensemble` cites no foreign address and
1811
+ * embeds 324,016 item references. The navigation is fetched for every package,
1812
+ * because every package publishes a site.
1813
+ *
1814
+ * Its own command rather than a step of `package compile` or `site`, so that
1815
+ * neither reaches the network. A build that downloads silently is not
1795
1816
  * reproducible, breaks offline, and hides a dependency's version change behind
1796
1817
  * a passing run.
1797
1818
  *
@@ -1838,11 +1859,34 @@ function depsCommand() {
1838
1859
  await fetchFromLocalArtifact(config, argv);
1839
1860
  return;
1840
1861
  }
1862
+ // First, because every package needs it and it depends on
1863
+ // nothing a repository declares: a dependency whose release
1864
+ // cannot be read stops the run after the navigation is cached,
1865
+ // not before.
1866
+ const navigation = await fetchNavigation(config);
1867
+ log.info(
1868
+ `Fetched the site navigation to ${path.relative(process.cwd(), navigation)}.`,
1869
+ );
1841
1870
  const indexes = await fetchAllMetadata(config);
1842
1871
  if (indexes) log.info(`Fetched ${indexes} dependency content index(es).`);
1843
1872
  const count = await fetchAllCatalogs(config);
1844
1873
  if (count) log.info(`Fetched ${count} dependency catalogue(s).`);
1845
- if (!indexes && !count) log.info("This package declares no dependencies.");
1874
+ if (!indexes && !count) {
1875
+ // Distinguished from a package declaring nothing at all: a
1876
+ // relationship may still be declared, just narrowed to the
1877
+ // Foundry manifest by `contentIndex: false` — reporting
1878
+ // "no dependencies" there would read as though the
1879
+ // declaration itself had gone missing.
1880
+ const declared = Object.values(config.relationships ?? {}).some(
1881
+ (entries) => entries?.length,
1882
+ );
1883
+ log.info(
1884
+ declared ?
1885
+ "No declared dependency needs a fetched content index or item " +
1886
+ "catalogue."
1887
+ : "This package declares no dependencies.",
1888
+ );
1889
+ }
1846
1890
  } catch (err) {
1847
1891
  reportFailure(err);
1848
1892
  process.exitCode = 1;
@@ -79,6 +79,7 @@ import { hideBin } from "yargs/helpers";
79
79
 
80
80
  import { loadPackageBuildConfig } from "../config.mjs";
81
81
  import { writeSiteRoot } from "../engine/site-root.mjs";
82
+ import { DEPLOY_ROOT } from "../engine/site-config.mjs";
82
83
  import { compilesFoundryDocuments } from "../content-config.mjs";
83
84
  import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
84
85
  import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
@@ -483,12 +484,12 @@ function siteRootCommand() {
483
484
  builder: (y) =>
484
485
  y.option("out", {
485
486
  type: "string",
486
- describe: "The directory that is deployed (default: build/site)",
487
+ describe: `The directory that is deployed (default: ${DEPLOY_ROOT})`,
487
488
  }),
488
489
  handler: handler(async (argv) => {
489
490
  const config = loadPackageBuildConfig();
490
491
  const shared = loadPackConfig();
491
- const out = path.resolve(config.rootDir, argv.out ?? "build/site");
492
+ const out = path.resolve(config.rootDir, argv.out ?? DEPLOY_ROOT);
492
493
 
493
494
  const { files } = writeSiteRoot({ pkg: shared.contentPackage, out });
494
495
  for (const file of files) {
package/config.mjs CHANGED
@@ -46,7 +46,7 @@
46
46
  * assetTransform: ./utils/svg-theme.mjs
47
47
  * stageDir: build/stage
48
48
  * clean:
49
- * extra: [site/content, site/public]
49
+ * extra: [coverage]
50
50
  * lang:
51
51
  * sources: lang/*.json
52
52
  * deploy:
@@ -91,6 +91,7 @@ const SECTION_KEYS = [
91
91
  export const DERIVED_MANIFEST_KEYS = Object.freeze({
92
92
  id: "`foundryPackage`, itself derived from package.json `name`",
93
93
  version: "package.json `version`",
94
+ description: "package.json `description`",
94
95
  url: "package.json `repository`",
95
96
  bugs: "package.json `repository`",
96
97
  manifest: "package.json `repository` and the release tag",
@@ -599,6 +600,56 @@ export function resolveAssetTransform(declared, rootDir) {
599
600
  return path.resolve(rootDir, declared);
600
601
  }
601
602
 
603
+ /**
604
+ * `package.json`'s `homepage`, checked against the address it must be.
605
+ *
606
+ * A package's Foundry manifest already derives its own `url` from
607
+ * `contentPackage` (`packageHomepage` in `manifest.mjs`); `homepage` states
608
+ * the same address a second time, in `package.json`, for the generated Hugo
609
+ * configuration to read a `baseURL` from without knowing where each
610
+ * repository keeps its own site configuration.
611
+ *
612
+ * Required unconditionally: every package publishes a site, so there is no
613
+ * package this does not apply to.
614
+ *
615
+ * **Not called by {@link resolvePackageBuildConfig}.** Every packaging
616
+ * command — `clean`, `deploy`, `manifest` and the rest — resolves through it,
617
+ * and none of them reads `homepage`: the Foundry manifest's own `url` is
618
+ * `packageHomepage(contentPackage)`, independent of it. The right caller is
619
+ * whatever reads `homepage` to write a site's `baseURL`.
620
+ *
621
+ * @param {string|null} homepage - The resolved `package.json` `homepage`, or
622
+ * `null` when none is declared.
623
+ * @param {string} contentPackage - The resolved `contentPackage`.
624
+ * @returns {void}
625
+ */
626
+ export function checkHomepage(homepage, contentPackage) {
627
+ if (homepage === null) {
628
+ fail(
629
+ "homepage",
630
+ "is not declared in `package.json`, and every package needs one " +
631
+ "to build its site's `baseURL` from. Add " +
632
+ `\`https://www.heroiclands.org/${contentPackage}/\``,
633
+ );
634
+ }
635
+
636
+ const suffix = `/${contentPackage}/`;
637
+ let pathname;
638
+ try {
639
+ pathname = new URL(homepage).pathname;
640
+ } catch {
641
+ fail("homepage", `is \`${homepage}\`, which is not an absolute URL`);
642
+ }
643
+ if (!pathname.endsWith(suffix)) {
644
+ fail(
645
+ "homepage",
646
+ `is \`${homepage}\`, but \`contentPackage\` is \`${contentPackage}\` — ` +
647
+ `a package's site is served at \`https://www.heroiclands.org${suffix}\`, ` +
648
+ `so \`package.json\`'s \`homepage\` must end \`${suffix}\``,
649
+ );
650
+ }
651
+ }
652
+
602
653
  /**
603
654
  * Resolve a package-build configuration from an already-loaded shared one.
604
655
  *