@heroiclands/package-build 22.0.3 → 22.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/manifest.mjs CHANGED
@@ -28,9 +28,10 @@
28
28
  *
29
29
  * - **Declared** — the `packageBuild.manifest` block, emitted unchanged, so a
30
30
  * key Foundry adds in a later version needs no release of this package.
31
- * - **Derived** — the identity, the version, the release addresses, the
32
- * compatibility ranges and the pack list. Declaring one of these is an error
33
- * rather than an override: the authored copy would be silently overwritten.
31
+ * - **Derived** — the identity, the description, the version, the release
32
+ * addresses, the compatibility ranges and the pack list. Declaring one of
33
+ * these is an error rather than an override: the authored copy would be
34
+ * silently overwritten.
34
35
  * - **Computed** — namespaced `flags` a repository works out for itself.
35
36
  *
36
37
  * **Nothing here invents an address.** The repository URL is read from
@@ -459,10 +460,10 @@ function withoutBuildKeys(entry) {
459
460
  *
460
461
  * - **Declared** — everything in `packageBuild.manifest`, emitted unchanged, so
461
462
  * a key Foundry adds later needs no release of this package.
462
- * - **Derived** — the identity, the release addresses, the version, the Foundry
463
- * and system compatibility ranges, and the pack list. These are refused if
464
- * also declared: an authored copy would be overwritten and the two would
465
- * disagree with nothing to say so.
463
+ * - **Derived** — the identity, the description, the release addresses, the
464
+ * version, the Foundry and system compatibility ranges, and the pack list.
465
+ * These are refused if also declared: an authored copy would be overwritten
466
+ * and the two would disagree with nothing to say so.
466
467
  * - **Computed** — namespaced `flags` a repository works out for itself, merged
467
468
  * over any it declared.
468
469
  *
@@ -492,6 +493,10 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
492
493
  artifact,
493
494
  }),
494
495
  };
496
+ // Own-property presence, not just value, decides whether a key survives
497
+ // into `ordered` below — an explicit `undefined` would still occupy a slot
498
+ // in it. Set only when `package.json` actually declares one.
499
+ if (packageJson.description !== undefined) derived.description = packageJson.description;
495
500
  if (config.compatibility) derived.compatibility = config.compatibility;
496
501
 
497
502
  // `requiresSystem` is the gate half of the declare/require split. It
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.0.3",
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",
@@ -147,6 +147,7 @@
147
147
  "markdown-it": "^15.0.0",
148
148
  "markdownlint-cli2": "^0.23.2",
149
149
  "prettier": "^3.9.6",
150
+ "smol-toml": "^1.7.0",
150
151
  "ssh2-sftp-client": "^12.1.1",
151
152
  "typescript": "^6.0.3",
152
153
  "unidecode": "^1.1.0",
package/stage.mjs CHANGED
@@ -42,9 +42,10 @@ import path from "node:path";
42
42
  /**
43
43
  * Directories every HeroicLands repository regenerates and none commits.
44
44
  *
45
- * A repository adds its own `sohl-thalorna` also clears the Hugo output
46
- * beneath `site/` — but these four are common to all of them because they come
47
- * from the shared toolchain rather than from any one package's layout.
45
+ * A repository adds its own through `packageBuild.clean.extra`, but these four
46
+ * are common to all of them because they come from the shared toolchain rather
47
+ * than from any one package's layout. Everything the site build writes — the
48
+ * Hugo source tree, Hugo's cache and the rendered site — is under `build/`.
48
49
  */
49
50
  export const BUILD_ARTIFACT_DIRS = Object.freeze(["build", ".vite", ".vitepress", ".rollup.cache"]);
50
51
 
@@ -10,6 +10,30 @@
10
10
  * @returns {string} An absolute path to import.
11
11
  */
12
12
  export function resolveAssetTransform(declared: string, rootDir: string): string;
13
+ /**
14
+ * `package.json`'s `homepage`, checked against the address it must be.
15
+ *
16
+ * A package's Foundry manifest already derives its own `url` from
17
+ * `contentPackage` (`packageHomepage` in `manifest.mjs`); `homepage` states
18
+ * the same address a second time, in `package.json`, for the generated Hugo
19
+ * configuration to read a `baseURL` from without knowing where each
20
+ * repository keeps its own site configuration.
21
+ *
22
+ * Required unconditionally: every package publishes a site, so there is no
23
+ * package this does not apply to.
24
+ *
25
+ * **Not called by {@link resolvePackageBuildConfig}.** Every packaging
26
+ * command — `clean`, `deploy`, `manifest` and the rest — resolves through it,
27
+ * and none of them reads `homepage`: the Foundry manifest's own `url` is
28
+ * `packageHomepage(contentPackage)`, independent of it. The right caller is
29
+ * whatever reads `homepage` to write a site's `baseURL`.
30
+ *
31
+ * @param {string|null} homepage - The resolved `package.json` `homepage`, or
32
+ * `null` when none is declared.
33
+ * @param {string} contentPackage - The resolved `contentPackage`.
34
+ * @returns {void}
35
+ */
36
+ export function checkHomepage(homepage: string | null, contentPackage: string): void;
13
37
  /**
14
38
  * Resolve a package-build configuration from an already-loaded shared one.
15
39
  *
@@ -73,6 +73,7 @@ export namespace DEFAULT_PATHS {
73
73
  let unpack: "build/tmp/packs";
74
74
  let foreignCache: "build/cache/foreign";
75
75
  let metadataCache: "build/cache/metadata";
76
+ let navigationCache: "build/cache/navigation";
76
77
  }
77
78
  /**
78
79
  * The Foundry document types a compendium pack may hold. This is the set the
@@ -175,6 +176,20 @@ export const SITE_MODES: readonly ["homepage", "content"];
175
176
  * @type {symbol}
176
177
  */
177
178
  export const DERIVED_SYSTEM_VERSION: symbol;
179
+ /**
180
+ * Hugo keys a repository may **not** declare under `site.hugo`, because the
181
+ * site build generates them and would only overwrite what was written.
182
+ *
183
+ * The same rule `DERIVED_MANIFEST_KEYS` states for the manifest, for the same
184
+ * reason: an authored `baseURL` would look authoritative, sit there unread,
185
+ * and disagree with the site forever. Each key names where its value comes
186
+ * from. A dotted key names a nested one, and covers everything beneath it —
187
+ * `params.brand` refuses `params.brand.logo` too — so `site.hugo` reaches only
188
+ * what the generator does not write.
189
+ *
190
+ * @type {Readonly<Record<string, string>>}
191
+ */
192
+ export const DERIVED_HUGO_KEYS: Readonly<Record<string, string>>;
178
193
  /**
179
194
  * How much of a package reaches the web.
180
195
  *
@@ -339,6 +354,11 @@ export type PathsInput = {
339
354
  * only those supplying a catalogue.
340
355
  */
341
356
  metadataCache?: string | undefined;
357
+ /**
358
+ * Where the site navigation is fetched
359
+ * to, for the generated Hugo menu.
360
+ */
361
+ navigationCache?: string | undefined;
342
362
  };
343
363
  /**
344
364
  * {@link PathsInput}, resolved to absolute paths against `rootDir`.
@@ -352,6 +372,7 @@ export type ResolvedPaths = {
352
372
  unpack: string;
353
373
  foreignCache: string;
354
374
  metadataCache: string;
375
+ navigationCache: string;
355
376
  };
356
377
  /**
357
378
  * The identity every compiled document's `_stats` block carries.
@@ -484,6 +505,17 @@ export type RelationshipSpec = {
484
505
  * `_stats.systemVersion` is stamped from.
485
506
  */
486
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;
487
519
  };
488
520
  /**
489
521
  * How a generated documentation page is framed in the repository publishing it.
@@ -590,6 +622,22 @@ export type ContentBuildConfigInput = {
590
622
  * package.
591
623
  */
592
624
  foundryPackage?: string | undefined;
625
+ /**
626
+ * `package.json`'s own `homepage` —
627
+ * the site build's `baseURL`. Checked
628
+ * by `checkHomepage` in
629
+ * `config.mjs`.
630
+ */
631
+ homepage?: string | undefined;
632
+ /**
633
+ * `package.json`'s own `author`, in
634
+ * either of npm's forms.
635
+ */
636
+ author?: string | {
637
+ name: string;
638
+ email?: string;
639
+ url?: string;
640
+ } | undefined;
593
641
  /**
594
642
  * Whether the package is a system, a
595
643
  * module, or documentation — the kind
@@ -683,6 +731,22 @@ export type ContentBuildConfig = {
683
731
  * package, which ships no Foundry package.
684
732
  */
685
733
  foundryPackage: string | null;
734
+ /**
735
+ * `package.json`'s own `homepage`,
736
+ * checked by `checkHomepage` in
737
+ * `config.mjs`.
738
+ */
739
+ homepage: string | null;
740
+ /**
741
+ * `package.json`'s own `author`, normalised
742
+ * from either of npm's forms; `null` when
743
+ * the package declares none.
744
+ */
745
+ author: Readonly<{
746
+ name: string;
747
+ email?: string;
748
+ url?: string;
749
+ }> | null;
686
750
  packageKind: PackageKind;
687
751
  /**
688
752
  * Derived, and **conditional**: the served
@@ -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
  *
@@ -105,8 +105,8 @@ export function collectHomepages(contentBase: string, ctx: object): {
105
105
  * has. Nothing is written at `/<package>/` itself: that becomes a redirect the
106
106
  * package's own repository authors, which is a routing fact rather than a page.
107
107
  *
108
- * @param {string} outRoot - The package's site root — the configured `site.out`,
109
- * one level above the content mount.
108
+ * @param {string} outRoot - The package's site root — the content mount's
109
+ * root, `build/hugo/content`, one level above the mount itself.
110
110
  * @param {readonly object[]} pages - From {@link collectHomepages}.
111
111
  * @param {object} config - The resolved configuration, for the package name and
112
112
  * the default title.
@@ -379,26 +379,6 @@ export function resolveSitePass(name: string | undefined, options: object): {
379
379
  beforeLinks?: Function;
380
380
  afterLinks?: Function;
381
381
  };
382
- /**
383
- * The output root, having established that it is safe to delete.
384
- *
385
- * The whole tree is a build artifact and is wiped on every run, so this
386
- * resolution is the difference between clearing a build directory and clearing
387
- * the repository. An unset `site.out` resolves to `rootDir` itself, and the
388
- * wipe then deletes the working tree — which is not a hypothetical: it happened
389
- * while this module was being written, on a configuration that simply had no
390
- * `site` section yet.
391
- *
392
- * So the path is refused unless it is **strictly inside** the repository root.
393
- * Both failing shapes are ordinary rather than exotic — an absent setting, and a
394
- * `..` that climbs out — and neither should be recoverable by being careful.
395
- *
396
- * @param {string} rootDir - The repository root.
397
- * @param {string} out - The configured `site.out`.
398
- * @returns {string} The absolute output root.
399
- * @throws {Error} When it is unset, or is not below `rootDir`.
400
- */
401
- export function resolveOutputRoot(rootDir: string, out: string): string;
402
382
  /**
403
383
  * Builds a Hugo content tree from a content tree, and reports what it found.
404
384
  *
@@ -410,18 +390,19 @@ export function resolveOutputRoot(rootDir: string, out: string): string;
410
390
  * @param {object} [options] - Options.
411
391
  * @param {object} [options.config] - A resolved configuration; loaded when
412
392
  * omitted.
413
- * @param {string} [options.outRoot] - Override the configured output mount.
414
393
  * @param {Map<string, object[]>} [options.sqlTables] - Prepared `sql` results,
415
394
  * keyed by the note's absolute file, from
416
395
  * {@link module:engine/sql-tables.prepareSqlTables}. A page authoring an
417
396
  * `sql` directive with none prepared is a table error: nothing here runs a
418
397
  * query.
419
398
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
420
- * 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.
421
403
  */
422
- export function buildSite({ config, outRoot, sqlTables }?: {
404
+ export function buildSite({ config, sqlTables }?: {
423
405
  config?: object | undefined;
424
- outRoot?: string | undefined;
425
406
  sqlTables?: Map<string, object[]> | undefined;
426
407
  }): {
427
408
  gates: object;
@@ -430,6 +411,7 @@ export function buildSite({ config, outRoot, sqlTables }?: {
430
411
  wikiErrors: object[];
431
412
  imageErrors: object[];
432
413
  manifests: object | null;
414
+ hasTags: boolean;
433
415
  };
434
416
  export { formatUnaddressableFinding };
435
417
  import { formatUnaddressableFinding } from "./metadata-index.mjs";