@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 CHANGED
@@ -1,5 +1,84 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b0ec919: **The site build generates the Hugo configuration.** `content-build site`
8
+ writes the whole Hugo source tree under `build/hugo/` — a generated
9
+ `hugo.toml`, the content mount at `build/hugo/content/`, Hugo's cache —
10
+ beside the deployment root `build/site/`, and the repository carries no Hugo
11
+ configuration of its own. Every value in the generated file has one source:
12
+ `package.json` (`homepage`, `description`, `author`),
13
+ `package-build.config.yaml` (`packageBuild.manifest.title`, `site.assets`,
14
+ `site.list`, `site.notfound`), the installed `@heroiclands/hugo-theme`, and
15
+ the organisation's brand links and navigation. `package.json`'s `homepage`
16
+ is checked on every site build: it must be
17
+ `https://www.heroiclands.org/<contentPackage>/`.
18
+
19
+ **`deps fetch` also fetches the navigation.** The header menu every site
20
+ renders comes from `https://www.heroiclands.org/nav.json`, cached under
21
+ `build/cache/navigation/` by `content-build deps fetch` and read from the
22
+ cache by the site build — a cold cache fails naming `deps fetch`, so a
23
+ package with no other dependency now runs `deps fetch` before `site` too.
24
+
25
+ **To migrate a repository:**
26
+
27
+ - Delete `site/` and its `.gitignore` entries (`site/content`, `site/public`,
28
+ `site/resources`), and any `packageBuild.clean.extra` entry naming them.
29
+ - Delete `site.out` from `package-build.config.yaml`; the location is fixed.
30
+ - Move the `[params.notfound]` block from the old `hugo.toml` to
31
+ `site.notfound` (`tagline`, `sitenoun`, optional `heroimage` and
32
+ `links[]` of `{title, url, text}`), `params.list.shortcodes` to
33
+ `site.list.shortcodes`, and `params.cdnBaseURL` to `site.assets` where it
34
+ was not already there. Anything else the file carried that the generator
35
+ does not write — `markup.goldmark.extensions.linkify`, say — goes under
36
+ `site.hugo`, which is deep-merged last; every key the generator writes is
37
+ refused there, naming its source.
38
+ - Point the npm scripts at the generated tree:
39
+ `hugo --source build/hugo --minify --gc --cleanDestinationDir` and
40
+ `hugo server --source build/hugo`.
41
+ - Make sure `package.json` declares `homepage`, `description` and `author`,
42
+ and that `@heroiclands/hugo-theme` is under `devDependencies`.
43
+ - a5e83fd: **A manifest's `description` comes from `package.json`.**
44
+ `packageBuild.manifest.description` is refused, the same way `id` and
45
+ `version` are — delete it; the manifest carries `package.json`'s own
46
+ `description`.
47
+
48
+ **`package.json`'s `homepage` and `author` are read and normalised.**
49
+ `homepage` names the address every package's site is served at — an
50
+ absolute URL ending `/<contentPackage>/` — and `author` is accepted in
51
+ either of npm's forms, a string or `{name, email, url}`, and normalised to
52
+ the object form.
53
+
54
+ ## 22.0.3
55
+
56
+ ### Patch Changes
57
+
58
+ - fd58dc0: **A package may ship assets and compile nothing.** `packs: []` is now a package
59
+ saying it has no documents, rather than one that forgot to say which, and a
60
+ package with no packs needs no content tree — its index is its asset records.
61
+ An alternative-art module is the case: the same addresses another package
62
+ publishes, resolving to different files when it is installed.
63
+
64
+ A package that _does_ declare packs is unchanged: a missing content tree is
65
+ still a misconfigured path and still fails, and an index that would state a
66
+ package has no content at all is still refused.
67
+ - cfdf061: **Two pieces of per-repository tooling move here.** Every site-publishing
68
+ package held its own copy of both, and a copy per consumer is a copy free to
69
+ drift — which is what happened, invisibly, because nobody reads all of them at
70
+ once.
71
+
72
+ - `packageBuild.assetTransform: svg-theme` names a shipped transform, so an
73
+ icon follows the reader's colour scheme without a module in the repository.
74
+ The value still takes a path, so a consumer with a transform of its own is
75
+ unaffected.
76
+ - `package-build site-root` writes a deployment's `_headers` and `_redirects`:
77
+ indexing suppressed on every host-assigned address, and both spellings of the
78
+ prefix root redirected to the landing with a lifetime on the 301. The package
79
+ name comes from `contentPackage`, which was the only thing the copies actually
80
+ varied.
81
+
3
82
  ## 22.0.2
4
83
 
5
84
  ### 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
@@ -2260,22 +2261,22 @@ address anyone links to. A package publishes its content index regardless — th
2260
2261
  licensing constraint is against publishing _pages_, not against the artifact
2261
2262
  existing — but nothing may declare it as a dependency.
2262
2263
 
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`
2264
+ The homepage's file is written at the root of the content mount,
2265
+ `build/hugo/content/` — the package's own site root, one level above the
2266
+ content mount, which is where `publish.address.prefix` puts everything else —
2267
+ under the name its address gives it, `homepage-root.md`. As with every other page, the front matter's `url`
2267
2268
  decides where it publishes, and states it relative to the site root — `site.base`
2268
2269
  does not reach it.
2269
2270
 
2270
2271
  **What it does not do is decide addresses.** Those come from `publish.address`,
2271
2272
  the same setting the content index reads, so a page and its index record cannot
2272
2273
  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:
2274
+ what a section is called, which extra trees are published beside the content,
2275
+ and the residue of the generated Hugo configuration that is this repository's
2276
+ own:
2275
2277
 
2276
2278
  ```yaml
2277
2279
  site:
2278
- out: kb/content # required; wiped on every run
2279
2280
  base: /sohl/ # default: /<contentPackage>/ — hrefs only, never a page's `url:`
2280
2281
  packages: [sohl, thalorna] # default: just contentPackage
2281
2282
  backfillSections: true
@@ -2294,11 +2295,16 @@ site:
2294
2295
  description: Folk, animals and the things that walk the world.
2295
2296
  readmeSections:
2296
2297
  dev-docs: { title: Developer Documentation, banner: banners/dev-docs.webp }
2298
+ list: { shortcodes: true }
2299
+ notfound:
2300
+ tagline: Song of Heroic Lands has no page at
2301
+ sitenoun: site
2302
+ links:
2303
+ - { title: Knowledgebase, url: kb/, text: Every section, with the full content catalog. }
2297
2304
  ```
2298
2305
 
2299
2306
  | Key | What it decides |
2300
2307
  | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2301
- | `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below. |
2302
2308
  | `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
2309
  | `packages` | Which content packages this site renders. Defaults to its own. |
2304
2310
  | `sections` | The Hugo sections this site declares, and what each says about itself — see below. |
@@ -2308,6 +2314,17 @@ site:
2308
2314
  | `trees` | Extra source trees published beside the content, preserving their source layout below a section. |
2309
2315
  | `pass` | A named bundle of this repository's own body rewrites. |
2310
2316
  | `passOptions` | That bundle's options. |
2317
+ | `assets` | The host every package's imagery is served from; the generated `params.cdnBaseURL`. |
2318
+ | `list` | How a listing page renders; the generated `params.list`. |
2319
+ | `notfound` | The wording of the "page not found" page; the generated `params.notfound`. |
2320
+ | `hugo` | A mapping deep-merged over the generated Hugo configuration, last. Every key the generator writes is refused here — see `docs/configuration.md`. |
2321
+
2322
+ Where the tree is written is not among them. `content-build site` writes the
2323
+ whole Hugo source tree under `build/hugo/` — the generated `hugo.toml`, the
2324
+ content mount at `build/hugo/content/`, Hugo's cache — and the consumer's
2325
+ script runs `hugo --source build/hugo` over it. The generated file's every
2326
+ value has a source the repository already states; `docs/configuration.md`
2327
+ lists them.
2311
2328
 
2312
2329
  ### What a section may declare
2313
2330
 
@@ -2409,15 +2426,14 @@ The **writers** name no keys: a section's `_index.md` is whatever the entry
2409
2426
  resolved to, `title` first. So extending the vocabulary is a change to the
2410
2427
  schema alone, and the two can no longer drift apart.
2411
2428
 
2412
- ### Why `out` is required
2429
+ ### Why the output location is fixed
2413
2430
 
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.
2431
+ The content mount is a build artifact and is **deleted on every run**, so that a
2432
+ page whose note was renamed cannot linger and keep publishing. A configurable
2433
+ location is a location that can be unset — and an unset one resolves to the
2434
+ repository root, where the wipe deletes the working tree. A fixed one under
2435
+ `build/` can point nowhere else, so `site.out` is refused by name and the wipe
2436
+ needs no guard.
2421
2437
 
2422
2438
  ### Consumer passes are named, not imported
2423
2439
 
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,14 @@ 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.
1488
1496
  *
1489
1497
  * **Each gate is reported and the run stops at the first that fires.** They are
1490
1498
  * ordered so the report names the cause rather than its symptoms: an unusable
@@ -1497,20 +1505,17 @@ function pdfCommand() {
1497
1505
  function siteCommand() {
1498
1506
  return {
1499
1507
  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) => {
1508
+ describe: "Build the Hugo source tree from the content tree",
1509
+ builder: () => {},
1510
+ handler: async () => {
1508
1511
  try {
1512
+ const config = loadPackConfig();
1513
+ const hugo = generateHugoConfig(config);
1509
1514
  const result = buildSite({
1510
- sqlTables: await prepareTreeSqlTables(loadPackConfig().paths.content, {
1511
- skipDirectories: loadPackConfig().skipDirectories,
1515
+ config,
1516
+ sqlTables: await prepareTreeSqlTables(config.paths.content, {
1517
+ skipDirectories: config.skipDirectories,
1512
1518
  }),
1513
- ...(argv.out ? { outRoot: argv.out } : {}),
1514
1519
  });
1515
1520
  const { gates } = result;
1516
1521
 
@@ -1623,6 +1628,8 @@ function siteCommand() {
1623
1628
  `${s.tree ?? 0} tree page(s) + ${s.landings} ` +
1624
1629
  `landing(s) to ${path.relative(process.cwd(), s.out)}`,
1625
1630
  );
1631
+ const { file } = writeHugoConfig(config, hugo);
1632
+ log.info(`wrote ${path.relative(process.cwd(), file)}`);
1626
1633
  } catch (err) {
1627
1634
  reportFailure(err);
1628
1635
  process.exitCode = 1;
@@ -1782,16 +1789,19 @@ async function fetchFromLocalArtifact(config, argv) {
1782
1789
 
1783
1790
  /**
1784
1791
  * `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
1792
+ * the **content index** of every declared dependency, the **item catalogue**
1793
+ * of those additionally declaring `itemCatalog: true`, and the **site
1794
+ * navigation** heroiclands.org publishes, which the site build writes its
1795
+ * menu from.
1796
+ *
1797
+ * The two dependency sets differ deliberately. Citing another package's
1798
+ * *addresses* and embedding its *items* are separate edges, and a package may
1799
+ * have either without the other — `harn-ensemble` cites no foreign address and
1800
+ * embeds 324,016 item references. The navigation is fetched for every package,
1801
+ * because every package publishes a site.
1802
+ *
1803
+ * Its own command rather than a step of `package compile` or `site`, so that
1804
+ * neither reaches the network. A build that downloads silently is not
1795
1805
  * reproducible, breaks offline, and hides a dependency's version change behind
1796
1806
  * a passing run.
1797
1807
  *
@@ -1838,6 +1848,14 @@ function depsCommand() {
1838
1848
  await fetchFromLocalArtifact(config, argv);
1839
1849
  return;
1840
1850
  }
1851
+ // First, because every package needs it and it depends on
1852
+ // nothing a repository declares: a dependency whose release
1853
+ // cannot be read stops the run after the navigation is cached,
1854
+ // not before.
1855
+ const navigation = await fetchNavigation(config);
1856
+ log.info(
1857
+ `Fetched the site navigation to ${path.relative(process.cwd(), navigation)}.`,
1858
+ );
1841
1859
  const indexes = await fetchAllMetadata(config);
1842
1860
  if (indexes) log.info(`Fetched ${indexes} dependency content index(es).`);
1843
1861
  const count = await fetchAllCatalogs(config);
@@ -78,6 +78,8 @@ import yargs from "yargs";
78
78
  import { hideBin } from "yargs/helpers";
79
79
 
80
80
  import { loadPackageBuildConfig } from "../config.mjs";
81
+ import { writeSiteRoot } from "../engine/site-root.mjs";
82
+ import { DEPLOY_ROOT } from "../engine/site-config.mjs";
81
83
  import { compilesFoundryDocuments } from "../content-config.mjs";
82
84
  import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
83
85
  import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
@@ -466,6 +468,37 @@ function manifestCommand() {
466
468
  };
467
469
  }
468
470
 
471
+ /**
472
+ * `package-build site-root` — the deployment's root files.
473
+ *
474
+ * Hugo owns everything under the `/<package>/` prefix; this owns what sits
475
+ * beside it, which is the pair Cloudflare Pages reads from the uploaded
476
+ * directory and nowhere else.
477
+ *
478
+ * @returns {object} The yargs command.
479
+ */
480
+ function siteRootCommand() {
481
+ return {
482
+ command: "site-root",
483
+ describe: "Write the deployment's _headers and _redirects",
484
+ builder: (y) =>
485
+ y.option("out", {
486
+ type: "string",
487
+ describe: `The directory that is deployed (default: ${DEPLOY_ROOT})`,
488
+ }),
489
+ handler: handler(async (argv) => {
490
+ const config = loadPackageBuildConfig();
491
+ const shared = loadPackConfig();
492
+ const out = path.resolve(config.rootDir, argv.out ?? DEPLOY_ROOT);
493
+
494
+ const { files } = writeSiteRoot({ pkg: shared.contentPackage, out });
495
+ for (const file of files) {
496
+ console.log(`✅ Wrote ${path.relative(config.rootDir, file)}.`);
497
+ }
498
+ }),
499
+ };
500
+ }
501
+
469
502
  /**
470
503
  * Every file matching a glob, as `{ path, text }` with the path relative to the
471
504
  * repository root — the form both the rules and the findings want.
@@ -1214,6 +1247,7 @@ yargs(hideBin(process.argv))
1214
1247
  .command(cleanCommand())
1215
1248
  .command(assetsCommand())
1216
1249
  .command(manifestCommand())
1250
+ .command(siteRootCommand())
1217
1251
  .command(schemaCommand())
1218
1252
  .command(langCommand())
1219
1253
  .command(labelsCommand())
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:
@@ -57,6 +57,7 @@
57
57
  */
58
58
 
59
59
  import path from "node:path";
60
+ import { fileURLToPath } from "node:url";
60
61
  import { loadPackConfig, locateConfigError, packConfigPath } from "./engine/pack-config.mjs";
61
62
 
62
63
  /** Keys the reserved section may declare. */
@@ -90,6 +91,7 @@ const SECTION_KEYS = [
90
91
  export const DERIVED_MANIFEST_KEYS = Object.freeze({
91
92
  id: "`foundryPackage`, itself derived from package.json `name`",
92
93
  version: "package.json `version`",
94
+ description: "package.json `description`",
93
95
  url: "package.json `repository`",
94
96
  bugs: "package.json `repository`",
95
97
  manifest: "package.json `repository` and the release tag",
@@ -565,6 +567,89 @@ function normalizeExceptions(value, field, where) {
565
567
  * collections, as collection → source directory.
566
568
  */
567
569
 
570
+ /**
571
+ * The asset transforms this package ships, by the name a consumer writes.
572
+ *
573
+ * `packageBuild.assetTransform` takes either one of these names or a path to a
574
+ * module of the consumer's own. A name is the answer where every package wants
575
+ * the same behaviour — theming an icon to the reader's colour scheme is not a
576
+ * per-package decision, and a copy in each consumer is a copy that drifts.
577
+ *
578
+ * @type {Readonly<Record<string, string>>}
579
+ */
580
+ export const BUILT_IN_ASSET_TRANSFORMS = Object.freeze({
581
+ "svg-theme": "./engine/svg-theme.mjs",
582
+ });
583
+
584
+ /**
585
+ * Where a declared `assetTransform` is loaded from.
586
+ *
587
+ * A built-in name resolves to the module this package ships. Anything else is a
588
+ * path, resolved against the repository root the way it always was — so a
589
+ * consumer with a transform of its own is unaffected.
590
+ *
591
+ * @param {string} declared - The authored value.
592
+ * @param {string} rootDir - The repository root.
593
+ * @returns {string} An absolute path to import.
594
+ */
595
+ export function resolveAssetTransform(declared, rootDir) {
596
+ const builtIn = BUILT_IN_ASSET_TRANSFORMS[declared];
597
+ if (builtIn) {
598
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), builtIn);
599
+ }
600
+ return path.resolve(rootDir, declared);
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
+
568
653
  /**
569
654
  * Resolve a package-build configuration from an already-loaded shared one.
570
655
  *
@@ -703,9 +788,9 @@ export function resolvePackageBuildConfig(shared) {
703
788
  assetTransform:
704
789
  section.assetTransform === undefined ?
705
790
  null
706
- : path.resolve(
707
- shared.rootDir,
791
+ : resolveAssetTransform(
708
792
  requireNonEmptyString(section.assetTransform, "packageBuild.assetTransform"),
793
+ shared.rootDir,
709
794
  ),
710
795
  manifest: normalizeManifest(section.manifest),
711
796
  manifestFlags: