@heroiclands/package-build 22.1.0 → 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,20 @@
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
+
3
18
  ## 22.1.0
4
19
 
5
20
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -1782,6 +1782,28 @@ asked for — a build that downloads silently is not reproducible and fails
1782
1782
  strangely offline. The cache is keyed by version, so changing the pinned version
1783
1783
  is a miss rather than a silent overwrite.
1784
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
+
1785
1807
  ### `packagebuild` needs no declaration
1786
1808
 
1787
1809
  package-build ships a set of images of its own — section banners chiefly — and a
@@ -1492,7 +1492,10 @@ function pdfCommand() {
1492
1492
  * **The Hugo configuration is generated before anything is written.** Its
1493
1493
  * sources — `package.json`'s `homepage`, the cached navigation, the installed
1494
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.
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.
1496
1499
  *
1497
1500
  * **Each gate is reported and the run stops at the first that fires.** They are
1498
1501
  * ordered so the report names the cause rather than its symptoms: an unusable
@@ -1510,7 +1513,14 @@ function siteCommand() {
1510
1513
  handler: async () => {
1511
1514
  try {
1512
1515
  const config = loadPackConfig();
1513
- const hugo = generateHugoConfig(config);
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);
1514
1524
  const result = buildSite({
1515
1525
  config,
1516
1526
  sqlTables: await prepareTreeSqlTables(config.paths.content, {
@@ -1628,6 +1638,7 @@ function siteCommand() {
1628
1638
  `${s.tree ?? 0} tree page(s) + ${s.landings} ` +
1629
1639
  `landing(s) to ${path.relative(process.cwd(), s.out)}`,
1630
1640
  );
1641
+ const hugo = generateHugoConfig(config, { hasTags: result.hasTags });
1631
1642
  const { file } = writeHugoConfig(config, hugo);
1632
1643
  log.info(`wrote ${path.relative(process.cwd(), file)}`);
1633
1644
  } catch (err) {
@@ -1860,7 +1871,22 @@ function depsCommand() {
1860
1871
  if (indexes) log.info(`Fetched ${indexes} dependency content index(es).`);
1861
1872
  const count = await fetchAllCatalogs(config);
1862
1873
  if (count) log.info(`Fetched ${count} dependency catalogue(s).`);
1863
- 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
+ }
1864
1890
  } catch (err) {
1865
1891
  reportFailure(err);
1866
1892
  process.exitCode = 1;
@@ -503,6 +503,14 @@ export function publishesContentPages(config) {
503
503
  * package this one targets — for a system
504
504
  * relationship, `verified` is what
505
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.
506
514
  */
507
515
 
508
516
  /**
@@ -780,6 +788,7 @@ const RELATIONSHIP_KEYS = [
780
788
  "manifest",
781
789
  "compatibility",
782
790
  "itemCatalog",
791
+ "contentIndex",
783
792
  ];
784
793
  const AUTHOR_KEYS = ["name", "email", "url"];
785
794
  const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
@@ -1604,7 +1613,9 @@ export const DERIVED_HUGO_KEYS = Object.freeze({
1604
1613
  contentDir: "the fixed content mount, `build/hugo/content`",
1605
1614
  themesDir: "where `@heroiclands/hugo-theme` is installed",
1606
1615
  theme: "the installed `@heroiclands/hugo-theme`",
1607
- disableKinds: "the toolchain, which renders the same kinds on every site",
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",
1608
1619
  "params.description": "package.json `description`",
1609
1620
  "params.author": "package.json `author`",
1610
1621
  "params.cdnBaseURL": "`site.assets`",
@@ -2147,6 +2158,24 @@ function normalizeRelationships(value) {
2147
2158
  }
2148
2159
  spec.itemCatalog = rel.itemCatalog;
2149
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
+ }
2150
2179
  return Object.freeze(spec);
2151
2180
  }),
2152
2181
  );
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
- | `metadataCacheDir` | `function metadataCacheDir(config, id, version)` | {string} The directory. | The cache directory for one dependency's index at one version. |
500
- | `isComplete` | `const isComplete` | {boolean} True when it was fetched to completion. | Whether a dependency's cache is present and complete. |
501
- | `markComplete` | `function markComplete(dir)` | {void} | Mark a dependency's cache complete. |
502
- | `cachedMetadataFiles` | `function cachedMetadataFiles(config)` | {string[]} One index file per declared dependency. | The fetched index files this build resolves foreign addresses against. |
503
- | `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. |
504
- | `newestVersionDir` | `function newestVersionDir(dirs)` | {string} The newest one. | The newest cached version among several version-keyed cache directories. |
505
- | `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. |
506
- | `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. |
507
- | `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. |
508
- | `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. |
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
 
@@ -834,14 +836,14 @@ Shared helpers for the pack compilers in `packages/content-build/`. The HeroicLa
834
836
 
835
837
  Wikilink resolution for the pack compilers. Content notes link to one another with wikilinks rather than file paths:
836
838
 
837
- | Export | Signature | Returns | Use it when |
838
- | -------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
839
- | `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. |
840
- | `readQualifier` | `function readQualifier(target, types, packages)` | {{type: string, shortcode: string, itemDoc: boolean, package?: string, system?: string, reason?: undefined} \| {reason: "unknown-type"} \| null} The resolved qualifier; a `reason` when the target is definitely qualified but names no known type; 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. |
841
- | `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. |
842
- | `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. |
843
- | `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. |
844
- | `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. |
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. |
845
847
 
846
848
  ### `engine.wikilinkSyntax`
847
849
 
@@ -956,24 +956,26 @@ the block cannot grow into a second configuration file:
956
956
  `content-build site` writes `build/hugo/hugo.toml` on every run. Every value
957
957
  in it has one source, and that source is where it is edited:
958
958
 
959
- | Key | Derived from |
960
- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
961
- | `baseURL` | `package.json` `homepage`, checked by `checkHomepage` — an absolute URL ending `/<contentPackage>/` |
962
- | `title` | `packageBuild.manifest.title`, which is required |
963
- | `locale` | the organisation's locale, `en-us`, in `engine/site-config.mjs` |
964
- | `publishDir` | `contentPackage`, under the deployment root `build/site` — written relative to `build/hugo/`, so `../site/<contentPackage>` |
965
- | `contentDir` | the fixed content mount, `build/hugo/content` — written as `content` |
966
- | `themesDir` | where `@heroiclands/hugo-theme` is installed, resolved the way Node resolves a package and written relative to `build/hugo/` |
967
- | `theme` | the installed `@heroiclands/hugo-theme`, so `hugo-theme` |
968
- | `disableKinds` | the toolchain, which renders the same kinds on every site: `["taxonomy", "term", "RSS"]` |
969
- | `params.description` | `package.json` `description`; absent when the package declares none |
970
- | `params.author` | `package.json` `author`, its `name`; absent when the package declares none |
971
- | `params.cdnBaseURL` | `site.assets`; absent when unset |
972
- | `params.brand` | the organisation's brand links `logo`, `licenseURL`, `discordURL` in `engine/site-config.mjs` |
973
- | `params.list` | `site.list` |
974
- | `params.notfound` | `site.notfound`; absent when undeclared |
975
- | `markup.goldmark.renderer.unsafe` | the toolchain, whose pages carry raw HTML — a `<figure>` for every image, a `<span>` marking an unresolved link |
976
- | `menu` | the navigation `content-build deps fetch` caches from `https://www.heroiclands.org/nav.json`, entry for entry, a dropdown's entries as `parent` entries |
959
+ | Key | Derived from |
960
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
961
+ | `baseURL` | `package.json` `homepage`, checked by `checkHomepage` — an absolute URL ending `/<contentPackage>/` |
962
+ | `title` | `packageBuild.manifest.title`, which is required |
963
+ | `locale` | the organisation's locale, `en-us`, in `engine/site-config.mjs` |
964
+ | `publishDir` | `contentPackage`, under the deployment root `build/site` — written relative to `build/hugo/`, so `../site/<contentPackage>` |
965
+ | `contentDir` | the fixed content mount, `build/hugo/content` — written as `content` |
966
+ | `themesDir` | where `@heroiclands/hugo-theme` is installed, resolved the way Node resolves a package and written relative to `build/hugo/` |
967
+ | `theme` | the installed `@heroiclands/hugo-theme`, so `hugo-theme` |
968
+ | `disableKinds` | whether any note in the tree carries `tags:`, which the site walk discovers: `["taxonomy", "term", "RSS"]` when none does, `["RSS"]` when at least one does |
969
+ | `taxonomies` | the same fact — written as `{ tag = "tags" }` when at least one note carries `tags:`, absent otherwise |
970
+ | `outputs` | the same fact — written as `{ taxonomy = ["HTML"], term = ["HTML"] }` when at least one note carries `tags:`, absent otherwise |
971
+ | `params.description` | `package.json` `description`; absent when the package declares none |
972
+ | `params.author` | `package.json` `author`, its `name`; absent when the package declares none |
973
+ | `params.cdnBaseURL` | `site.assets`; absent when unset |
974
+ | `params.brand` | the organisation's brand links — `logo`, `licenseURL`, `discordURL` — in `engine/site-config.mjs` |
975
+ | `params.list` | `site.list` |
976
+ | `params.notfound` | `site.notfound`; absent when undeclared |
977
+ | `markup.goldmark.renderer.unsafe` | the toolchain, whose pages carry raw HTML — a `<figure>` for every image, a `<span>` marking an unresolved link |
978
+ | `menu` | the navigation `content-build deps fetch` caches from `https://www.heroiclands.org/nav.json`, entry for entry, a dropdown's entries as `parent` entries |
977
979
 
978
980
  The site build reads the navigation from the cache only. A cold cache is an
979
981
  error naming the command that fills it:
@@ -989,9 +991,13 @@ none fails the site build:
989
991
 
990
992
  > ``package-build config: `packageBuild.manifest.title` is not declared, and the site's `title` reads from it.``
991
993
 
992
- Nothing else is emitted. `[taxonomies]` and `[outputs]` are Hugo's defaults
993
- once the taxonomy kinds and RSS are disabled, and every other key is
994
- `site.hugo`'s to add.
994
+ Nothing else is emitted. A site whose notes carry no `tags:` publishes no
995
+ taxonomy pages `[taxonomies]` and `[outputs]` go unwritten, and Hugo's
996
+ defaults never apply because `taxonomy` and `term` are disabled kinds. A site
997
+ with at least one tagged note publishes `/tags/` and a page per tag: Hugo's
998
+ own default taxonomy pair also declares `category`, so `[taxonomies]` names
999
+ only `tag`, and `[outputs]` restricts both to `HTML` so neither produces a
1000
+ feed. Every other key is `site.hugo`'s to add.
995
1001
 
996
1002
  ### `pdf`
997
1003
 
@@ -1149,6 +1155,7 @@ Each entry, in any of the four lists:
1149
1155
  | `relationships.systems[].manifest` | string | no | none |
1150
1156
  | `relationships.systems[].compatibility` | object, `{minimum?, verified?}` | no | none |
1151
1157
  | `relationships.systems[].itemCatalog` | boolean | no | `false` |
1158
+ | `relationships.systems[].contentIndex` | boolean | no | `true` |
1152
1159
 
1153
1160
  (the same keys apply under `requires[]`, `recommends[]` and
1154
1161
  `conflicts[]`.)
@@ -1157,7 +1164,7 @@ Each entry, in any of the four lists:
1157
1164
 
1158
1165
  > ``package-build config: `relationships.<kind>[<index>].id` must be a non-empty string.``
1159
1166
 
1160
- > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, contentPackage, type, manifest, compatibility, itemCatalog).``
1167
+ > ``package-build config: `relationships.<kind>[<index>].<key>` is not a recognized option (expected one of: id, contentPackage, type, manifest, compatibility, itemCatalog, contentIndex).``
1161
1168
 
1162
1169
  `contentPackage` names what the other package's _content_ is called, where
1163
1170
  that differs from its Foundry id. A note addresses a file by the content
@@ -1176,6 +1183,20 @@ item catalogue at build time. It requires a `manifest`:
1176
1183
 
1177
1184
  > ``package-build config: `relationships.<kind>[<index>].itemCatalog` needs a `manifest` naming the package to fetch.``
1178
1185
 
1186
+ `contentIndex` and `itemCatalog` are the two edges a relationship may declare,
1187
+ and a package may have either without the other. `itemCatalog` says a
1188
+ dependency supplies _items_; `contentIndex`, `true` by default, says
1189
+ `deps fetch` fetches its published note index and this tree may cite its
1190
+ addresses by wikilink. Declaring `contentIndex: false` narrows the
1191
+ relationship to the Foundry manifest only — a dependency Foundry installs but
1192
+ this tree never cites — so `deps fetch` fetches nothing for it and a wikilink
1193
+ into it fails, naming the key, rather than resolving against a stale
1194
+ declaration or an index nobody fetched:
1195
+
1196
+ > ``package-build config: `relationships.<kind>[<index>].contentIndex` must be true or false.``
1197
+
1198
+ > ``package-build config: `relationships.<kind>[<index>].contentIndex` cannot be false together with `itemCatalog: true` — a catalogue is fetched from the same index.``
1199
+
1179
1200
  ### `systems`
1180
1201
 
1181
1202
  **Type:** object (`{id: spec}`) · **Optional** · default `{}`. Refused in a
@@ -1647,17 +1668,17 @@ the source directory:
1647
1668
 
1648
1669
  ## Every retired or forbidden key, in one place
1649
1670
 
1650
- | Key | Why |
1651
- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1652
- | `publish.address.landing` | Retired — named a whole-section landing, and there are no sections to address. |
1653
- | `packs[].folders` | Retired — a folder is a note (`type: folder`), materialised by the pack whose documents reference it. |
1654
- | `rootDir` | Forbidden in a YAML configuration — always the file's own directory. |
1655
- | `foundryPackage` | Forbidden in a YAML configuration — always the adjacent `package.json` `name`. |
1656
- | `homepage`, `author` | Forbidden in a YAML configuration — always the adjacent `package.json`'s own `homepage` and `author`. |
1657
- | `stats.systemId` | Forbidden in every configuration — derived from `packageKind`, `requiresSystem` or a lone declared system. |
1658
- | `stats.systemVersion` | Forbidden in every configuration — derived from `package.json` (a system) or `systems:` / `relationships.systems` (a module). |
1659
- | `packageBuild.manifest.id`, `.version`, `.description`, `.url`, `.bugs`, `.manifest`, `.download`, `.compatibility`, `.relationships`, `.packs` | Forbidden — each is derived from `package.json` or the top level of `package-build.config.yaml`; see [`packageBuild.manifest`](#packagebuildmanifest). |
1660
- | `site.out` | Retired — the site build writes its content mount at `build/hugo/content`, beside the generated `hugo.toml`. |
1661
- | `site.hugo.baseURL`, `.title`, `.locale`, `.publishDir`, `.contentDir`, `.themesDir`, `.theme`, `.disableKinds`, `.params.description`, `.params.author`, `.params.cdnBaseURL`, `.params.brand`, `.params.list`, `.params.notfound`, `.markup.goldmark.renderer.unsafe`, `.menu` | Forbidden — each is written by the site build from a source it names; see [the generated Hugo configuration](#the-generated-hugo-configuration). |
1662
- | `publish.site: true` / `publish.site: false` | Refused rather than mapped — write `homepage` or `content`. |
1663
- | `packs`, `itemBuilders`, `docs`, `compatibility`, `relationships`, `systems`, `requiresSystem`, `stats`, `foundryPackage` | Forbidden in a `documentation` package — each describes a Foundry package this kind is not; see the key's own section for its located refusal message. |
1671
+ | Key | Why |
1672
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
1673
+ | `publish.address.landing` | Retired — named a whole-section landing, and there are no sections to address. |
1674
+ | `packs[].folders` | Retired — a folder is a note (`type: folder`), materialised by the pack whose documents reference it. |
1675
+ | `rootDir` | Forbidden in a YAML configuration — always the file's own directory. |
1676
+ | `foundryPackage` | Forbidden in a YAML configuration — always the adjacent `package.json` `name`. |
1677
+ | `homepage`, `author` | Forbidden in a YAML configuration — always the adjacent `package.json`'s own `homepage` and `author`. |
1678
+ | `stats.systemId` | Forbidden in every configuration — derived from `packageKind`, `requiresSystem` or a lone declared system. |
1679
+ | `stats.systemVersion` | Forbidden in every configuration — derived from `package.json` (a system) or `systems:` / `relationships.systems` (a module). |
1680
+ | `packageBuild.manifest.id`, `.version`, `.description`, `.url`, `.bugs`, `.manifest`, `.download`, `.compatibility`, `.relationships`, `.packs` | Forbidden — each is derived from `package.json` or the top level of `package-build.config.yaml`; see [`packageBuild.manifest`](#packagebuildmanifest). |
1681
+ | `site.out` | Retired — the site build writes its content mount at `build/hugo/content`, beside the generated `hugo.toml`. |
1682
+ | `site.hugo.baseURL`, `.title`, `.locale`, `.publishDir`, `.contentDir`, `.themesDir`, `.theme`, `.disableKinds`, `.taxonomies`, `.outputs`, `.params.description`, `.params.author`, `.params.cdnBaseURL`, `.params.brand`, `.params.list`, `.params.notfound`, `.markup.goldmark.renderer.unsafe`, `.menu` | Forbidden — each is written by the site build from a source it names; see [the generated Hugo configuration](#the-generated-hugo-configuration). |
1683
+ | `publish.site: true` / `publish.site: false` | Refused rather than mapped — write `homepage` or `content`. |
1684
+ | `packs`, `itemBuilders`, `docs`, `compatibility`, `relationships`, `systems`, `requiresSystem`, `stats`, `foundryPackage` | Forbidden in a `documentation` package — each describes a Foundry package this kind is not; see the key's own section for its located refusal message. |
@@ -86,7 +86,7 @@ import {
86
86
  PACKAGE_BASE,
87
87
  readCanonicalKey,
88
88
  } from "./content-address.mjs";
89
- import { loadForeignIndexes } from "./metadata-index.mjs";
89
+ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
90
90
  import { frontmatterWikilinks, slugify } from "./web-wikilinks.mjs";
91
91
  import { homepageAddresses, isHomepage } from "./homepage.mjs";
92
92
  import { RETIRED_TYPES } from "./ids.mjs";
@@ -268,6 +268,10 @@ export function buildLinkIndex(
268
268
  for (const v of foreign.index.values()) if (v.type) types.add(v.type);
269
269
 
270
270
  const packages = new Set([...(byKey.size ? [pkg] : []), ...foreign.packages]);
271
+ // Packages declared `contentIndex: false` — a Foundry dependency only, with
272
+ // no fetched index. A link naming one is refused with a diagnostic that
273
+ // names the key, rather than reading as an undeclared package or a typo.
274
+ const noIndexPackages = config ? noContentIndexPackages(config) : new Set();
271
275
 
272
276
  // The address space an `![[…]]` embed resolves against, shaped as every
273
277
  // other asset resolver reads one so the checker cannot answer an authored
@@ -464,7 +468,7 @@ export function buildLinkIndex(
464
468
  * @returns {object|undefined} The note it addresses.
465
469
  */
466
470
  function resolveAddress(target, keyPath) {
467
- const qualified = readQualifier(target, types, packages);
471
+ const qualified = readQualifier(target, types, packages, noIndexPackages);
468
472
  if (!qualified || qualified.reason) return undefined;
469
473
  // Every omitted segment defaults from where the link is written,
470
474
  // so the target expands to exactly one canonical address and this is a
@@ -501,7 +505,7 @@ export function buildLinkIndex(
501
505
  * @returns {object[]} The foreign entries, each carrying its `package`.
502
506
  */
503
507
  function foreignHits(target, keyPath) {
504
- const q = readQualifier(target, types, packages);
508
+ const q = readQualifier(target, types, packages, noIndexPackages);
505
509
  if (!q || q.reason) return [];
506
510
  // An omitted package means *this* package, so a short form
507
511
  // addresses nothing foreign and never reaches a dependency's index.
@@ -552,7 +556,7 @@ export function buildLinkIndex(
552
556
  * @returns {object|null} The note, asset record or foreign entry declaring it.
553
557
  */
554
558
  function referenceHit(target) {
555
- const q = readQualifier(target, types, packages);
559
+ const q = readQualifier(target, types, packages, noIndexPackages);
556
560
  if (!q || q.reason) return null;
557
561
  const local = matchAddress([...byKey, ...byAssetKey], q);
558
562
  if (local.length) return local[0][1];
@@ -566,6 +570,8 @@ export function buildLinkIndex(
566
570
  anchors,
567
571
  types,
568
572
  packages,
573
+ /** Packages declared `contentIndex: false`, a Foundry dependency only. */
574
+ noIndexPackages,
569
575
  /**
570
576
  * The files this package ships, by canonical address. Separate from the
571
577
  * notes because the two record shapes are read differently, and exposed
@@ -597,7 +603,7 @@ export function buildLinkIndex(
597
603
  foreignHits,
598
604
  referenceHit,
599
605
  /** Whether a target reads as a qualified address at all. */
600
- isAddress: (target) => Boolean(readQualifier(target, types, packages)),
606
+ isAddress: (target) => Boolean(readQualifier(target, types, packages, noIndexPackages)),
601
607
  };
602
608
  }
603
609
 
@@ -922,8 +928,9 @@ export function auditHomepageLinks(index) {
922
928
  * which addresses a foreign manifest answered. Each `deadAddresses` entry
923
929
  * carries a `reason` from {@link LINK_FINDING_REASONS} —
924
930
  * `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
925
- * `packages`), or `"unresolved"` — and every one of them is an **error**:
926
- * the three resolvers agree on severity for every class.
931
+ * `packages`), `"no-content-index"`, or `"unresolved"` — and every one of
932
+ * them is an **error**: the three resolvers agree on severity for every
933
+ * class.
927
934
  */
928
935
  export function auditLinks(index) {
929
936
  const { notes, anchors, linksOf, embedsOf, resolve, manifestHit, isAddress } = index;
@@ -1004,10 +1011,13 @@ export function auditLinks(index) {
1004
1011
  });
1005
1012
  continue;
1006
1013
  }
1007
- const read = readQualifier(target, index.types, index.packages);
1014
+ const read = readQualifier(target, index.types, index.packages, index.noIndexPackages);
1008
1015
  deadAddresses.push({
1009
1016
  ...at,
1010
- reason: read?.reason === "unknown-type" ? "unknown-type" : "unresolved",
1017
+ reason:
1018
+ read?.reason === "unknown-type" ? "unknown-type"
1019
+ : read?.reason === "no-content-index" ? "no-content-index"
1020
+ : "unresolved",
1011
1021
  });
1012
1022
  }
1013
1023
  }
@@ -43,7 +43,7 @@ import { contentPackage, foundryPackageId } from "./content-package.mjs";
43
43
  import { searchableFrontmatter } from "./note-package.mjs";
44
44
  import { PACKAGE_BASE } from "./content-address.mjs";
45
45
  import { resolveNoteId } from "./note-ids.mjs";
46
- import { loadForeignIndexes } from "./metadata-index.mjs";
46
+ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
47
47
  // The record accessors only — deriving records reaches the pack router and the
48
48
  // manifest emitter, which reach the compilers, which load this module. Reading
49
49
  // a record needs none of that.
@@ -787,6 +787,7 @@ export function buildContentLinkIndex(
787
787
  );
788
788
  return buildWikilinkIndex(docs, resolved.foundryPackage, foreign, resolved.contentPackage, {
789
789
  assets,
790
+ noIndexPackages: noContentIndexPackages(resolved),
790
791
  });
791
792
  }
792
793
 
@@ -84,6 +84,11 @@ export const METADATA_RELATIONSHIP_KINDS = Object.freeze(["systems", "requires"]
84
84
  * and needing no items is the mirror of it. Gating the index on the catalogue
85
85
  * flag would serve neither.
86
86
  *
87
+ * **Excludes a relationship declaring `contentIndex: false`.** That opts a
88
+ * dependency out of both edges at once: it is a Foundry dependency only, cited
89
+ * by neither a wikilink nor an item reference, so there is nothing here for
90
+ * `deps fetch` to fill and no cache this build will ever read.
91
+ *
87
92
  * The declaration is the one already in the emitted `system.json` /
88
93
  * `module.json`, so it cannot drift from what Foundry itself installs, and
89
94
  * there is no new configuration key to keep in step. Each entry carries the
@@ -98,6 +103,7 @@ export function metadataRelationships(config) {
98
103
  const out = [];
99
104
  for (const kind of METADATA_RELATIONSHIP_KINDS) {
100
105
  for (const rel of config?.relationships?.[kind] ?? []) {
106
+ if (rel.contentIndex === false) continue;
101
107
  out.push({
102
108
  id: rel.id,
103
109
  manifest: rel.manifest,
@@ -109,6 +115,32 @@ export function metadataRelationships(config) {
109
115
  return out;
110
116
  }
111
117
 
118
+ /**
119
+ * Every package a relationship declares `contentIndex: false` on, keyed by
120
+ * the content package name a link into it would use.
121
+ *
122
+ * A separate set from {@link metadataRelationships}, which answers "what does
123
+ * `deps fetch` fill" — this answers "what does the link resolver recognise as
124
+ * a package with no fetched index", which a wikilink checker or pack compiler
125
+ * needs to tell that case apart from a package nobody declared at all.
126
+ *
127
+ * Walked across every relationship kind, not only the citable ones: the
128
+ * config validation refuses the flag nowhere by kind, so a resolver reading it
129
+ * back should not assume one either.
130
+ *
131
+ * @param {object} config - The resolved build configuration.
132
+ * @returns {ReadonlySet<string>} The content package names.
133
+ */
134
+ export function noContentIndexPackages(config) {
135
+ const out = new Set();
136
+ for (const entries of Object.values(config?.relationships ?? {})) {
137
+ for (const rel of entries ?? []) {
138
+ if (rel.contentIndex === false) out.add(rel.contentPackage ?? rel.id);
139
+ }
140
+ }
141
+ return Object.freeze(out);
142
+ }
143
+
112
144
  /**
113
145
  * The cache directory for one dependency's index at one version.
114
146
  *