@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.
@@ -496,6 +496,26 @@ export function hasTag(fm, tag) {
496
496
  return false;
497
497
  }
498
498
 
499
+ /**
500
+ * Whether a note carries any `tags:` at all, however authored.
501
+ *
502
+ * The one question the site build asks of tags in aggregate — whether the
503
+ * tree publishes taxonomy pages — rather than about a particular tag. Reads
504
+ * `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
505
+ * or a blank string as carrying none.
506
+ *
507
+ * @param {object|null|undefined} fm - Parsed frontmatter.
508
+ * @returns {boolean} Whether the note carries at least one tag.
509
+ */
510
+ export function hasAnyTag(fm) {
511
+ const raw = fm?.tags ?? fm?.tag;
512
+ if (raw == null) return false;
513
+ for (const entry of Array.isArray(raw) ? raw : [raw]) {
514
+ if (typeof entry === "string" && entry.trim() !== "") return true;
515
+ }
516
+ return false;
517
+ }
518
+
499
519
  /**
500
520
  * Whether a note is tagged as an unfinished **draft**.
501
521
  *
@@ -57,13 +57,14 @@ import { renderImageFigures } from "./content-images.mjs";
57
57
  import { pathnameProblem, resolvePathname } from "./pathnames.mjs";
58
58
  import { buildSiteIndex, resolveInfoboxRef, wikiContext } from "./site-index.mjs";
59
59
  import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
60
- import { loadForeignIndexes } from "./metadata-index.mjs";
60
+ import { loadForeignIndexes, noContentIndexPackages } from "./metadata-index.mjs";
61
61
  import { noteInfoboxes } from "./infobox-registry.mjs";
62
62
  import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
63
63
  import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
64
64
  import { loadPackConfig } from "./pack-config.mjs";
65
65
  import { routerFor } from "./pack-router.mjs";
66
66
  import { searchableFrontmatter } from "./note-package.mjs";
67
+ import { hasAnyTag } from "./note-vocabulary.mjs";
67
68
  // The corpus, from the one pass that derives it.
68
69
  import { indexRecordsFor } from "./content-index.mjs";
69
70
  import { isNoteRecord, noteFile } from "./index-records.mjs";
@@ -476,7 +477,10 @@ export function siteGates(pages, findings, { config }) {
476
477
  out.unaddressable = unaddressableForeignPackages(foreign.index);
477
478
  if (out.unaddressable.length) return out;
478
479
 
479
- const index = buildSiteIndex(pages, { foreignIndex: foreign.index });
480
+ const index = buildSiteIndex(pages, {
481
+ foreignIndex: foreign.index,
482
+ noIndexPackages: noContentIndexPackages(config),
483
+ });
480
484
  out.conflicts = index.conflicts;
481
485
  if (out.conflicts.length) return out;
482
486
 
@@ -1043,7 +1047,10 @@ export function resolveSitePass(name, options) {
1043
1047
  * `sql` directive with none prepared is a table error: nothing here runs a
1044
1048
  * query.
1045
1049
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
1046
- * wikiErrors: object[], imageErrors: object[], manifests: object|null}}
1050
+ * wikiErrors: object[], imageErrors: object[], manifests: object|null,
1051
+ * hasTags: boolean}} `hasTags` is whether any note the walk read carries
1052
+ * `tags:` — what {@link module:engine/site-config.hugoConfig} reads to
1053
+ * decide whether the site emits taxonomy pages.
1047
1054
  */
1048
1055
  export function buildSite({ config, sqlTables } = {}) {
1049
1056
  const resolved = config ?? loadPackConfig();
@@ -1142,6 +1149,7 @@ export function buildSite({ config, sqlTables } = {}) {
1142
1149
  wikiErrors: [],
1143
1150
  imageErrors: [],
1144
1151
  stats: null,
1152
+ hasTags: homepages.some((p) => hasAnyTag(p.fm)),
1145
1153
  };
1146
1154
  }
1147
1155
 
@@ -1164,6 +1172,7 @@ export function buildSite({ config, sqlTables } = {}) {
1164
1172
  landings: 0,
1165
1173
  out: homeRoot,
1166
1174
  },
1175
+ hasTags: homepages.some((p) => hasAnyTag(p.fm)),
1167
1176
  };
1168
1177
  }
1169
1178
 
@@ -1211,6 +1220,7 @@ export function buildSite({ config, sqlTables } = {}) {
1211
1220
  tableErrors: [],
1212
1221
  wikiErrors: [],
1213
1222
  imageErrors: [],
1223
+ hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
1214
1224
  };
1215
1225
  }
1216
1226
 
@@ -1269,6 +1279,7 @@ export function buildSite({ config, sqlTables } = {}) {
1269
1279
  landings,
1270
1280
  out,
1271
1281
  },
1282
+ hasTags: [...pages, ...homepageEntries].some((p) => hasAnyTag(p.fm)),
1272
1283
  };
1273
1284
  }
1274
1285
 
@@ -83,14 +83,39 @@ export const BRAND = Object.freeze({
83
83
  });
84
84
 
85
85
  /**
86
- * The kinds no site renders.
86
+ * The kinds a site with no tagged notes renders.
87
87
  *
88
88
  * A section exists only where `site.sections` declares one, so a tree holding
89
- * only the homepage emits nothing beyond it; taxonomies and feeds would be
90
- * empty shells on every site.
89
+ * only the homepage emits nothing beyond it; a taxonomy nobody's notes fill
90
+ * and a feed would be empty shells. A site whose notes carry `tags:` emits
91
+ * `taxonomy` and `term` after all — see {@link hugoConfig} — but `RSS` is
92
+ * disabled either way: nothing here publishes a feed.
91
93
  */
92
94
  export const DISABLE_KINDS = Object.freeze(["taxonomy", "term", "RSS"]);
93
95
 
96
+ /**
97
+ * The kinds a site with at least one tagged note renders — everything but
98
+ * `RSS`.
99
+ */
100
+ const DISABLE_KINDS_TAGGED = Object.freeze(["RSS"]);
101
+
102
+ /**
103
+ * The single taxonomy a tagged site declares.
104
+ *
105
+ * Only `tag` — Hugo's default pair also declares `category`, which nothing
106
+ * here authors and which would publish an empty `/categories/`.
107
+ */
108
+ const TAXONOMIES = Object.freeze({ tag: "tags" });
109
+
110
+ /**
111
+ * The taxonomy output formats a tagged site declares — `HTML` only, so no
112
+ * feed is produced for `/tags/` or a single tag.
113
+ */
114
+ const TAXONOMY_OUTPUTS = Object.freeze({
115
+ taxonomy: Object.freeze(["HTML"]),
116
+ term: Object.freeze(["HTML"]),
117
+ });
118
+
94
119
  /**
95
120
  * The markup settings the toolchain's own output requires.
96
121
  *
@@ -359,11 +384,14 @@ function deepMerge(base, overrides) {
359
384
  * @param {string} [options.description] - `package.json`'s `description`.
360
385
  * @param {readonly NavigationEntry[]} options.navigation - The navigation.
361
386
  * @param {string} options.themesDir - From {@link resolveThemesDir}.
387
+ * @param {boolean} [options.hasTags] - Whether any note the site build walked
388
+ * carries `tags:`, from {@link module:engine/site-build.buildSite}'s
389
+ * `hasTags`. Defaults to `false` — no tagged note, no taxonomy pages.
362
390
  * @returns {Record<string, any>} The configuration Hugo reads.
363
391
  * @throws {TypeError} When `homepage` fails `checkHomepage`, or the
364
392
  * configuration declares no `packageBuild.manifest.title`.
365
393
  */
366
- export function hugoConfig({ config, description, navigation, themesDir }) {
394
+ export function hugoConfig({ config, description, navigation, themesDir, hasTags = false }) {
367
395
  checkHomepage(config.homepage, config.contentPackage);
368
396
 
369
397
  const title = config.packageBuild?.manifest?.title;
@@ -391,11 +419,18 @@ export function hugoConfig({ config, description, navigation, themesDir }) {
391
419
  themesDir,
392
420
  theme: THEME,
393
421
  contentDir: path.posix.relative(HUGO_SOURCE, HUGO_CONTENT),
394
- disableKinds: [...DISABLE_KINDS],
422
+ disableKinds: hasTags ? [...DISABLE_KINDS_TAGGED] : [...DISABLE_KINDS],
395
423
  params,
396
424
  markup: structuredClone(MARKUP),
397
425
  menu: { main: menuEntries(navigation) },
398
426
  };
427
+ if (hasTags) {
428
+ generated.taxonomies = { ...TAXONOMIES };
429
+ generated.outputs = {
430
+ taxonomy: [...TAXONOMY_OUTPUTS.taxonomy],
431
+ term: [...TAXONOMY_OUTPUTS.term],
432
+ };
433
+ }
399
434
  return deepMerge(generated, config.site.hugo);
400
435
  }
401
436
 
@@ -434,15 +469,21 @@ function packageDescription(rootDir) {
434
469
  * intact.
435
470
  *
436
471
  * @param {object} config - The resolved build configuration.
472
+ * @param {object} [options] - Options.
473
+ * @param {boolean} [options.hasTags] - Whether any note the site build walked
474
+ * carries `tags:`. Defaults to `false`, so a caller generating the
475
+ * configuration before the walk (to fail fast on a missing source) gets the
476
+ * untagged shape; pass the site build's own `hasTags` once it is known.
437
477
  * @returns {Record<string, any>} The configuration Hugo reads.
438
478
  * @throws {Error} When any source is missing or wrong.
439
479
  */
440
- export function generateHugoConfig(config) {
480
+ export function generateHugoConfig(config, { hasTags = false } = {}) {
441
481
  return hugoConfig({
442
482
  config,
443
483
  description: packageDescription(config.rootDir),
444
484
  navigation: readCachedNavigation(config),
445
485
  themesDir: resolveThemesDir(config.rootDir),
486
+ hasTags,
446
487
  });
447
488
  }
448
489
 
@@ -166,9 +166,16 @@ function mergeForeign(index, foreignIndex) {
166
166
  * @param {Map<string, {package: string, type?: string}>} [options.foreignIndex]
167
167
  * The merged index from `loadForeignIndexes`. Omit when the build publishes
168
168
  * no cross-package links.
169
+ * @param {Set<string>} [options.noIndexPackages] - Packages declared
170
+ * `contentIndex: false` — a Foundry dependency only, with no fetched index.
171
+ * A link naming one fails naming the key, rather than reading as prose or an
172
+ * ordinary dead address.
169
173
  * @returns {SiteIndex} The index, and what could not be addressed unambiguously.
170
174
  */
171
- export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
175
+ export function buildSiteIndex(
176
+ entries,
177
+ { foreignIndex = new Map(), noIndexPackages = new Set() } = {},
178
+ ) {
172
179
  const index = new Map();
173
180
  const contentTypes = new Set();
174
181
  const sections = new Set();
@@ -298,6 +305,7 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
298
305
  contentTypes,
299
306
  sections,
300
307
  packages,
308
+ noIndexPackages,
301
309
  refIndex,
302
310
  conflicts,
303
311
  };
@@ -347,6 +355,7 @@ export function wikiContext(
347
355
  sections: built.sections,
348
356
  contentTypes: built.contentTypes,
349
357
  packages: built.packages,
358
+ noIndexPackages: built.noIndexPackages,
350
359
  // The package a link written on this page defaults to when it names
351
360
  // none. Taken from the resolved configuration, the same source
352
361
  // the index's own addresses are built from, so a bare link cannot
@@ -279,9 +279,10 @@ function isPlainMap(value) {
279
279
  *
280
280
  * **Every target that resolves nowhere fails the build**, and is
281
281
  * classified into the vocabulary all three resolvers share — `unlabelled`,
282
- * `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`. Failures are
283
- * collected in `ctx.errors`, each carrying the authored `link` and its
284
- * `occurrence` so a caller can report the line and column it sits on.
282
+ * `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`,
283
+ * `no-content-index`. Failures are collected in `ctx.errors`, each carrying
284
+ * the authored `link` and its `occurrence` so a caller can report the line and
285
+ * column it sits on.
285
286
  *
286
287
  * There is deliberately no exception letting a hyphen-form address through while
287
288
  * any linkable package had no vendored manifest, since a real cross-package
@@ -312,9 +313,12 @@ function isPlainMap(value) {
312
313
  *
313
314
  * @param {string} body - The markdown body.
314
315
  * @param {object} ctx - `{ index, assets, collide, sections, contentTypes,
315
- * packages, foreign, type, errors, src, file }`.
316
+ * packages, noIndexPackages, foreign, type, errors, src, file }`.
316
317
  * `packages` is every package an address may name, without which the leading
317
- * package segment of a canonical address reads as an unknown type; `foreign`
318
+ * package segment of a canonical address reads as an unknown type;
319
+ * `noIndexPackages` is every package declared `contentIndex: false`, so a
320
+ * qualified address naming one fails with `no-content-index` rather than
321
+ * `not-an-address`; `foreign`
318
322
  * is the cross-package manifest index; `assets` is the address space an embed
319
323
  * resolves against. `src` is the page's display
320
324
  * path and `file` the source file a diagnostic should name — absent, `src`
@@ -402,7 +406,12 @@ export function resolveWebWikilinks(body, ctx) {
402
406
  // The canonical separator has to be resolved, not merely
403
407
  // recognised. `null` here means the target is not an address at all,
404
408
  // which is a defect: there is no other namespace to try.
405
- const read = readQualifier(target, ctx.contentTypes ?? new Set(), ctx.packages);
409
+ const read = readQualifier(
410
+ target,
411
+ ctx.contentTypes ?? new Set(),
412
+ ctx.packages,
413
+ ctx.noIndexPackages,
414
+ );
406
415
  const rawKey = target.toLowerCase();
407
416
  const hit =
408
417
  lookupRead(ctx.index, read, ctx.contentPackage) ??
@@ -478,6 +487,7 @@ export function resolveWebWikilinks(body, ctx) {
478
487
  // a key: a partial address has no single key to be non-null.
479
488
  : (read && !read.reason) || siteAddress ? "unresolved"
480
489
  : read?.reason === "unknown-type" ? "unknown-type"
490
+ : read?.reason === "no-content-index" ? "no-content-index"
481
491
  // Every link is an address, and this is not one. Distinct from
482
492
  // a dead address, because the fix is different: a name has to
483
493
  // become an address, not be corrected.
@@ -184,6 +184,9 @@ export function unlabelledLinkMessage(target) {
184
184
  * - `unresolved` — parses as an address, and nothing publishes it.
185
185
  * - `ambiguous` — more than one package publishes the short address.
186
186
  * - `unknown-anchor` — the address resolved, the `#section` it names did not.
187
+ * - `no-content-index` — the address names a package declared
188
+ * `contentIndex: false`, a Foundry dependency only, so no index was fetched
189
+ * for it to resolve against.
187
190
  *
188
191
  * @type {ReadonlySet<string>}
189
192
  */
@@ -197,6 +200,7 @@ export const LINK_FINDING_REASONS = Object.freeze(
197
200
  "unresolved",
198
201
  "ambiguous",
199
202
  "unknown-anchor",
203
+ "no-content-index",
200
204
  ]),
201
205
  );
202
206
 
@@ -312,6 +316,12 @@ export function linkFindingMessage({ reason, target, packages, anchor, type }) {
312
316
  );
313
317
  case "unresolved":
314
318
  return unresolvedAddressMessage(target);
319
+ case "no-content-index":
320
+ return (
321
+ `address [[${target}]] names a package declared \`contentIndex: false\` — ` +
322
+ `it is a Foundry dependency only, and no content index was fetched for it, ` +
323
+ `so nothing it publishes can be cited`
324
+ );
315
325
  default:
316
326
  throw new Error(
317
327
  `linkFindingMessage: "${reason}" is not one of ` +
@@ -203,13 +203,18 @@ export function resolveItemDocType(qualifier, types) {
203
203
  * @param {Set<string>} types - Every type the content tree contains.
204
204
  * @param {Set<string>} [packages] - Every package an address may name. Omitted
205
205
  * by callers that resolve within one package, where the form cannot occur.
206
+ * @param {Set<string>} [noIndexPackages] - Packages declared `contentIndex:
207
+ * false` — a Foundry dependency only, with no fetched index. A fully
208
+ * qualified target naming one is refused with `no-content-index` before its
209
+ * type is even considered, since there is no index to resolve it against.
206
210
  * @returns {{type: string, shortcode: string, itemDoc: boolean,
207
211
  * package?: string, system?: string, reason?: undefined}
208
- * | {reason: "unknown-type"} | null}
212
+ * | {reason: "unknown-type"|"no-content-index", package?: string} | null}
209
213
  * The resolved qualifier; a `reason` when the target is definitely qualified
210
- * but names no known type; or `null` when it is not an address at all.
214
+ * but names no known type or no fetched index; or `null` when it is not an
215
+ * address at all.
211
216
  */
212
- export function readQualifier(target, types, packages) {
217
+ export function readQualifier(target, types, packages, noIndexPackages) {
213
218
  // **Package, system and type are lowercase; the shortcode is not.** A
214
219
  // shortcode is case-sensitive and routinely mixed — `Clb`, `LtShoe`,
215
220
  // `HsTunic` — so it is written as the note declares it. The three segments
@@ -221,7 +226,7 @@ export function readQualifier(target, types, packages) {
221
226
  // (`[[Shock State]]`), and calling that a badly-cased address rather than
222
227
  // not an address would name the wrong mistake. Neither tree carries a
223
228
  // violation — 10,538 authored targets — so this pins a rule already kept.
224
- const read = readQualifierCased(target, types, packages);
229
+ const read = readQualifierCased(target, types, packages, noIndexPackages);
225
230
  if (read && !read.reason && qualifyingSegments(target).some((s) => /[A-Z]/.test(s))) {
226
231
  return { reason: "not-lowercase" };
227
232
  }
@@ -249,9 +254,10 @@ function qualifyingSegments(target) {
249
254
  * @param {string} target
250
255
  * @param {Set<string>} types
251
256
  * @param {Set<string>} [packages]
257
+ * @param {Set<string>} [noIndexPackages]
252
258
  * @returns {object|null}
253
259
  */
254
- function readQualifierCased(target, types, packages) {
260
+ function readQualifierCased(target, types, packages, noIndexPackages) {
255
261
  // The slash form is legacy and states neither package nor system, so it is
256
262
  // read first and separately. A slash is unconditionally a qualifier —
257
263
  // nothing else uses one — which is why an unknown type before it is
@@ -281,6 +287,10 @@ function readQualifierCased(target, types, packages) {
281
287
  // qualified.
282
288
  case 4: {
283
289
  const pkg = norm(parts[0]);
290
+ // Checked before the type: a package with no fetched index has no
291
+ // vocabulary to resolve the rest of the target against, and the
292
+ // fix is the config declaration, not the shortcode.
293
+ if (noIndexPackages?.has(pkg)) return { reason: "no-content-index", package: pkg };
284
294
  if (!packages?.has(pkg)) return null;
285
295
  const system = norm(parts[1]);
286
296
  if (!isSystemSegment(system)) return null;
@@ -359,11 +369,21 @@ export function anchorPageId(noteId, anchorSlug) {
359
369
  * @param {Map<string, object>} [opts.assets] - The files this package ships, by
360
370
  * canonical address. They resolve no link — an asset is not a document — and
361
371
  * answer only the art fields, which name a file and never a document.
372
+ * @param {Set<string>} [opts.noIndexPackages] - Packages declared
373
+ * `contentIndex: false` — a Foundry dependency only. A link naming one fails
374
+ * with `no-content-index` rather than resolving, ambiguously, as either a
375
+ * typo or an undeclared package.
362
376
  * @returns {{byShortcode: Map<string, object>, types: Set<string>}} `types` is
363
377
  * every type the tree actually contains, so a qualifier naming no real type
364
378
  * can be told apart from a missing target.
365
379
  */
366
- export function buildWikilinkIndex(docs, packageId, foreign, contentPackage, { assets } = {}) {
380
+ export function buildWikilinkIndex(
381
+ docs,
382
+ packageId,
383
+ foreign,
384
+ contentPackage,
385
+ { assets, noIndexPackages } = {},
386
+ ) {
367
387
  if (!packageId) {
368
388
  throw new Error(
369
389
  "buildWikilinkIndex: packageId is required — it is the first " +
@@ -454,6 +474,8 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage, { a
454
474
  foreign: foreignByKey,
455
475
  /** The files this package ships, by canonical address. */
456
476
  assets: assets ?? new Map(),
477
+ /** Packages declared `contentIndex: false`, a Foundry dependency only. */
478
+ noIndexPackages: noIndexPackages ?? new Set(),
457
479
  };
458
480
  }
459
481
 
@@ -656,7 +678,12 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
656
678
  if (target === "" && slug) {
657
679
  doc = { type, id, pack, docPack };
658
680
  } else {
659
- const qualified = readQualifier(target, index.types, index.packages);
681
+ const qualified = readQualifier(
682
+ target,
683
+ index.types,
684
+ index.packages,
685
+ index.noIndexPackages,
686
+ );
660
687
  qualifiedRead = qualified;
661
688
  // A target that does not parse as an address is a defect: there is
662
689
  // no second namespace left to fall through to.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.1.0",
3
+ "version": "22.1.1",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -505,6 +505,17 @@ export type RelationshipSpec = {
505
505
  * `_stats.systemVersion` is stamped from.
506
506
  */
507
507
  compatibility?: CompatibilitySpec | undefined;
508
+ /**
509
+ * Whether `deps fetch` fetches this
510
+ * dependency's content index. Default
511
+ * `true`. `false` declares the dependency
512
+ * for the Foundry manifest only — nothing
513
+ * this tree cites by wikilink — and refuses
514
+ * `itemCatalog: true` on the same entry,
515
+ * since a catalogue is fetched from the same
516
+ * index.
517
+ */
518
+ contentIndex?: boolean | undefined;
508
519
  };
509
520
  /**
510
521
  * How a generated documentation page is framed in the repository publishing it.
@@ -144,8 +144,9 @@ export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Ar
144
144
  * which addresses a foreign manifest answered. Each `deadAddresses` entry
145
145
  * carries a `reason` from {@link LINK_FINDING_REASONS} —
146
146
  * `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
147
- * `packages`), or `"unresolved"` — and every one of them is an **error**:
148
- * the three resolvers agree on severity for every class.
147
+ * `packages`), `"no-content-index"`, or `"unresolved"` — and every one of
148
+ * them is an **error**: the three resolvers agree on severity for every
149
+ * class.
149
150
  */
150
151
  export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
151
152
  deadAnchors: object[];
@@ -9,6 +9,11 @@
9
9
  * and needing no items is the mirror of it. Gating the index on the catalogue
10
10
  * flag would serve neither.
11
11
  *
12
+ * **Excludes a relationship declaring `contentIndex: false`.** That opts a
13
+ * dependency out of both edges at once: it is a Foundry dependency only, cited
14
+ * by neither a wikilink nor an item reference, so there is nothing here for
15
+ * `deps fetch` to fill and no cache this build will ever read.
16
+ *
12
17
  * The declaration is the one already in the emitted `system.json` /
13
18
  * `module.json`, so it cannot drift from what Foundry itself installs, and
14
19
  * there is no new configuration key to keep in step. Each entry carries the
@@ -25,6 +30,23 @@ export function metadataRelationships(config: object): Array<{
25
30
  kind: string;
26
31
  verified: string | undefined;
27
32
  }>;
33
+ /**
34
+ * Every package a relationship declares `contentIndex: false` on, keyed by
35
+ * the content package name a link into it would use.
36
+ *
37
+ * A separate set from {@link metadataRelationships}, which answers "what does
38
+ * `deps fetch` fill" — this answers "what does the link resolver recognise as
39
+ * a package with no fetched index", which a wikilink checker or pack compiler
40
+ * needs to tell that case apart from a package nobody declared at all.
41
+ *
42
+ * Walked across every relationship kind, not only the citable ones: the
43
+ * config validation refuses the flag nowhere by kind, so a resolver reading it
44
+ * back should not assume one either.
45
+ *
46
+ * @param {object} config - The resolved build configuration.
47
+ * @returns {ReadonlySet<string>} The content package names.
48
+ */
49
+ export function noContentIndexPackages(config: object): ReadonlySet<string>;
28
50
  /**
29
51
  * The cache directory for one dependency's index at one version.
30
52
  *
@@ -50,6 +50,18 @@ export function exclusiveTagGroups(type: string, groups?: object): {
50
50
  * @returns {boolean} Whether the note carries it.
51
51
  */
52
52
  export function hasTag(fm: object | null | undefined, tag: string): boolean;
53
+ /**
54
+ * Whether a note carries any `tags:` at all, however authored.
55
+ *
56
+ * The one question the site build asks of tags in aggregate — whether the
57
+ * tree publishes taxonomy pages — rather than about a particular tag. Reads
58
+ * `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
59
+ * or a blank string as carrying none.
60
+ *
61
+ * @param {object|null|undefined} fm - Parsed frontmatter.
62
+ * @returns {boolean} Whether the note carries at least one tag.
63
+ */
64
+ export function hasAnyTag(fm: object | null | undefined): boolean;
53
65
  /**
54
66
  * Whether a note is tagged as an unfinished **draft**.
55
67
  *
@@ -396,7 +396,10 @@ export function resolveSitePass(name: string | undefined, options: object): {
396
396
  * `sql` directive with none prepared is a table error: nothing here runs a
397
397
  * query.
398
398
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
399
- * wikiErrors: object[], imageErrors: object[], manifests: object|null}}
399
+ * wikiErrors: object[], imageErrors: object[], manifests: object|null,
400
+ * hasTags: boolean}} `hasTags` is whether any note the walk read carries
401
+ * `tags:` — what {@link module:engine/site-config.hugoConfig} reads to
402
+ * decide whether the site emits taxonomy pages.
400
403
  */
401
404
  export function buildSite({ config, sqlTables }?: {
402
405
  config?: object | undefined;
@@ -408,6 +411,7 @@ export function buildSite({ config, sqlTables }?: {
408
411
  wikiErrors: object[];
409
412
  imageErrors: object[];
410
413
  manifests: object | null;
414
+ hasTags: boolean;
411
415
  };
412
416
  export { formatUnaddressableFinding };
413
417
  import { formatUnaddressableFinding } from "./metadata-index.mjs";
@@ -92,15 +92,19 @@ export function resolveThemesDir(rootDir: string): string;
92
92
  * @param {string} [options.description] - `package.json`'s `description`.
93
93
  * @param {readonly NavigationEntry[]} options.navigation - The navigation.
94
94
  * @param {string} options.themesDir - From {@link resolveThemesDir}.
95
+ * @param {boolean} [options.hasTags] - Whether any note the site build walked
96
+ * carries `tags:`, from {@link module:engine/site-build.buildSite}'s
97
+ * `hasTags`. Defaults to `false` — no tagged note, no taxonomy pages.
95
98
  * @returns {Record<string, any>} The configuration Hugo reads.
96
99
  * @throws {TypeError} When `homepage` fails `checkHomepage`, or the
97
100
  * configuration declares no `packageBuild.manifest.title`.
98
101
  */
99
- export function hugoConfig({ config, description, navigation, themesDir }: {
102
+ export function hugoConfig({ config, description, navigation, themesDir, hasTags }: {
100
103
  config: object;
101
104
  description?: string | undefined;
102
105
  navigation: readonly NavigationEntry[];
103
106
  themesDir: string;
107
+ hasTags?: boolean | undefined;
104
108
  }): Record<string, any>;
105
109
  /**
106
110
  * The configuration as the TOML Hugo reads.
@@ -118,10 +122,17 @@ export function hugoToml(generated: Record<string, unknown>): string;
118
122
  * intact.
119
123
  *
120
124
  * @param {object} config - The resolved build configuration.
125
+ * @param {object} [options] - Options.
126
+ * @param {boolean} [options.hasTags] - Whether any note the site build walked
127
+ * carries `tags:`. Defaults to `false`, so a caller generating the
128
+ * configuration before the walk (to fail fast on a missing source) gets the
129
+ * untagged shape; pass the site build's own `hasTags` once it is known.
121
130
  * @returns {Record<string, any>} The configuration Hugo reads.
122
131
  * @throws {Error} When any source is missing or wrong.
123
132
  */
124
- export function generateHugoConfig(config: object): Record<string, any>;
133
+ export function generateHugoConfig(config: object, { hasTags }?: {
134
+ hasTags?: boolean | undefined;
135
+ }): Record<string, any>;
125
136
  /**
126
137
  * Write `build/hugo/hugo.toml`.
127
138
  *
@@ -162,11 +173,13 @@ export const BRAND: Readonly<{
162
173
  discordURL: "https://discord.gg/EwMfkNd3az";
163
174
  }>;
164
175
  /**
165
- * The kinds no site renders.
176
+ * The kinds a site with no tagged notes renders.
166
177
  *
167
178
  * A section exists only where `site.sections` declares one, so a tree holding
168
- * only the homepage emits nothing beyond it; taxonomies and feeds would be
169
- * empty shells on every site.
179
+ * only the homepage emits nothing beyond it; a taxonomy nobody's notes fill
180
+ * and a feed would be empty shells. A site whose notes carry `tags:` emits
181
+ * `taxonomy` and `term` after all — see {@link hugoConfig} — but `RSS` is
182
+ * disabled either way: nothing here publishes a feed.
170
183
  */
171
184
  export const DISABLE_KINDS: readonly string[];
172
185
  /**
@@ -7,13 +7,18 @@
7
7
  * @param {Map<string, {package: string, type?: string}>} [options.foreignIndex]
8
8
  * The merged index from `loadForeignIndexes`. Omit when the build publishes
9
9
  * no cross-package links.
10
+ * @param {Set<string>} [options.noIndexPackages] - Packages declared
11
+ * `contentIndex: false` — a Foundry dependency only, with no fetched index.
12
+ * A link naming one fails naming the key, rather than reading as prose or an
13
+ * ordinary dead address.
10
14
  * @returns {SiteIndex} The index, and what could not be addressed unambiguously.
11
15
  */
12
- export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex }?: {
16
+ export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex, noIndexPackages }?: {
13
17
  foreignIndex?: Map<string, {
14
18
  package: string;
15
19
  type?: string;
16
20
  }> | undefined;
21
+ noIndexPackages?: Set<string> | undefined;
17
22
  }): SiteIndex;
18
23
  /**
19
24
  * The per-page context a wikilink resolver takes.
@@ -44,9 +44,10 @@ export function frontmatterWikilinks(fm: unknown): Array<{
44
44
  *
45
45
  * **Every target that resolves nowhere fails the build**, and is
46
46
  * classified into the vocabulary all three resolvers share — `unlabelled`,
47
- * `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`. Failures are
48
- * collected in `ctx.errors`, each carrying the authored `link` and its
49
- * `occurrence` so a caller can report the line and column it sits on.
47
+ * `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`,
48
+ * `no-content-index`. Failures are collected in `ctx.errors`, each carrying
49
+ * the authored `link` and its `occurrence` so a caller can report the line and
50
+ * column it sits on.
50
51
  *
51
52
  * There is deliberately no exception letting a hyphen-form address through while
52
53
  * any linkable package had no vendored manifest, since a real cross-package
@@ -77,9 +78,12 @@ export function frontmatterWikilinks(fm: unknown): Array<{
77
78
  *
78
79
  * @param {string} body - The markdown body.
79
80
  * @param {object} ctx - `{ index, assets, collide, sections, contentTypes,
80
- * packages, foreign, type, errors, src, file }`.
81
+ * packages, noIndexPackages, foreign, type, errors, src, file }`.
81
82
  * `packages` is every package an address may name, without which the leading
82
- * package segment of a canonical address reads as an unknown type; `foreign`
83
+ * package segment of a canonical address reads as an unknown type;
84
+ * `noIndexPackages` is every package declared `contentIndex: false`, so a
85
+ * qualified address naming one fails with `no-content-index` rather than
86
+ * `not-an-address`; `foreign`
83
87
  * is the cross-package manifest index; `assets` is the address space an embed
84
88
  * resolves against. `src` is the page's display
85
89
  * path and `file` the source file a diagnostic should name — absent, `src`
@@ -223,6 +223,9 @@ export const WIKILINK: RegExp;
223
223
  * - `unresolved` — parses as an address, and nothing publishes it.
224
224
  * - `ambiguous` — more than one package publishes the short address.
225
225
  * - `unknown-anchor` — the address resolved, the `#section` it names did not.
226
+ * - `no-content-index` — the address names a package declared
227
+ * `contentIndex: false`, a Foundry dependency only, so no index was fetched
228
+ * for it to resolve against.
226
229
  *
227
230
  * @type {ReadonlySet<string>}
228
231
  */