@heroiclands/package-build 22.0.2 → 22.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ *
4
+ * SPDX-License-Identifier: GPL-3.0-or-later
5
+ */
6
+
7
+ /**
8
+ * The files that belong to a deployment's **root** rather than to the rendered
9
+ * site.
10
+ *
11
+ * Hugo renders into `<out>/<package>/`, because the deployment carries the
12
+ * `/<package>/` prefix physically and the routing layer is a path-preserving
13
+ * pass-through. The directory that is *uploaded* is its parent, and Cloudflare
14
+ * Pages reads `_headers` and `_redirects` from there and nowhere else — a copy
15
+ * inside the prefix is published as a text file and never applied. Hugo owns
16
+ * everything under the prefix; this owns what sits beside it.
17
+ *
18
+ * **One implementation, because it is one policy.** What is indexable, where
19
+ * the prefix root sends a reader, and how long that answer is cached are
20
+ * decisions about the hosting rather than about any one package. Held in each
21
+ * consumer they are the same file with one constant changed, which is a file
22
+ * that drifts — and the drift is invisible, because nobody reads all of the
23
+ * copies at once.
24
+ *
25
+ * @module
26
+ */
27
+
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+
31
+ /**
32
+ * The namespace the routing layer derives a package's origin in.
33
+ *
34
+ * `/<package>/` on the public host is proxied to
35
+ * `https://<package>.<suffix>/<package>/`, and {@link noindexHeaders} depends on
36
+ * that being a dedicated namespace.
37
+ *
38
+ * @type {string}
39
+ */
40
+ export const ORIGIN_SUFFIX = "pkg.heroiclands.org";
41
+
42
+ /**
43
+ * Where a package's landing is served, now that it is an addressed page.
44
+ *
45
+ * The site build emits the homepage at its own address rather than as the
46
+ * site root's `_index.md`, so the prefix root is a redirect to it.
47
+ *
48
+ * @param {string} pkg - The content package name.
49
+ * @returns {string} The landing's path.
50
+ */
51
+ export function landingPath(pkg) {
52
+ return `/${pkg}/homepage-root/`;
53
+ }
54
+
55
+ /**
56
+ * Suppress indexing of every address a deployment answers on but nobody
57
+ * advertises.
58
+ *
59
+ * Cloudflare Pages assigns three: the project's own `pages.dev`, a per-
60
+ * deployment `pages.dev`, and the custom domain the project carries so the
61
+ * routing layer has an origin to fetch. None is advertised, all answer with the
62
+ * same pages, and left alone they are indexed and compete with the canonical
63
+ * URL in search results.
64
+ *
65
+ * The third matters most: it is the address the routing layer fetches, so it is
66
+ * the host-assigned address a reader is most plausibly handed. The rules are
67
+ * **scoped to those hostnames**, which keeps this correct for anyone deploying
68
+ * the site under a domain of their own — there it is indexable, and only the
69
+ * host-assigned addresses are not.
70
+ *
71
+ * @returns {string[]} The header block's lines.
72
+ */
73
+ export function noindexHeaders() {
74
+ return [
75
+ "https://:project.pages.dev/*",
76
+ " X-Robots-Tag: noindex",
77
+ "",
78
+ "https://:version.:project.pages.dev/*",
79
+ " X-Robots-Tag: noindex",
80
+ "",
81
+ `https://:package.${ORIGIN_SUFFIX}/*`,
82
+ " X-Robots-Tag: noindex",
83
+ "",
84
+ ];
85
+ }
86
+
87
+ /**
88
+ * The lifetime pinned on the prefix-root redirect, and why it is pinned.
89
+ *
90
+ * Cloudflare Pages sets no `Cache-Control` on a redirect it generates — those
91
+ * responses carry `location` and nothing else — and a 301 with no lifetime is
92
+ * cached by a browser indefinitely, on the most-linked URL there is. An hour
93
+ * keeps the 301's canonical signal without the permanence.
94
+ *
95
+ * @param {string} pkg - The content package name.
96
+ * @returns {string[]} The header block's lines.
97
+ */
98
+ export function cacheHeaders(pkg) {
99
+ return [
100
+ `/${pkg}/`,
101
+ " Cache-Control: max-age=3600",
102
+ "",
103
+ `/${pkg}`,
104
+ " Cache-Control: max-age=3600",
105
+ "",
106
+ ];
107
+ }
108
+
109
+ /**
110
+ * Both forms of the prefix root, because Pages matches the raw path.
111
+ *
112
+ * Redirect matching runs before any trailing-slash or `index.html` handling, so
113
+ * `/<pkg>` and `/<pkg>/` are distinct keys and a rule on one does not catch the
114
+ * other.
115
+ *
116
+ * @param {string} pkg - The content package name.
117
+ * @returns {string} The `_redirects` file's contents.
118
+ */
119
+ export function redirects(pkg) {
120
+ const to = landingPath(pkg);
121
+ return [`/${pkg}/ ${to} 301`, `/${pkg} ${to} 301`, ""].join("\n");
122
+ }
123
+
124
+ /**
125
+ * The `_headers` file's contents.
126
+ *
127
+ * @param {string} pkg - The content package name.
128
+ * @returns {string} The file's contents.
129
+ */
130
+ export function headers(pkg) {
131
+ return [...noindexHeaders(), ...cacheHeaders(pkg)].join("\n");
132
+ }
133
+
134
+ /**
135
+ * Write `_headers` and `_redirects` beside the rendered site.
136
+ *
137
+ * @param {object} options - Options.
138
+ * @param {string} options.pkg - The content package name, which is also the
139
+ * directory Hugo rendered into.
140
+ * @param {string} options.out - The directory that is deployed.
141
+ * @returns {{files: string[]}} The files written.
142
+ * @throws {Error} When no rendered site is there, which means the site build
143
+ * has not run and writing root files would publish a deployment with nothing
144
+ * under the prefix.
145
+ */
146
+ export function writeSiteRoot({ pkg, out }) {
147
+ const root = path.resolve(out);
148
+ const rendered = path.join(root, pkg);
149
+ if (!fs.existsSync(path.join(rendered, "index.html"))) {
150
+ throw new Error(
151
+ `${out}/${pkg}/ holds no rendered site — build the site before its root files`,
152
+ );
153
+ }
154
+
155
+ const written = [];
156
+ for (const [name, body] of [
157
+ ["_headers", headers(pkg)],
158
+ ["_redirects", redirects(pkg)],
159
+ ]) {
160
+ const file = path.join(root, name);
161
+ fs.writeFileSync(file, body);
162
+ written.push(file);
163
+ }
164
+ return { files: written };
165
+ }
@@ -0,0 +1,140 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * A staged SVG follows the reader's colour scheme.
16
+ *
17
+ * An icon drawn as black line art disappears against a dark background, and
18
+ * Foundry themes its own chrome. So a shape that is explicitly black, or black
19
+ * by default because it declares no `fill`, gets a rule that paints it the
20
+ * ink colour for the scheme in use.
21
+ *
22
+ * **Named rather than pathed.** `packageBuild.assetTransform: svg-theme`
23
+ * reaches this module. Every package that themes its icons themes them the same
24
+ * way — the ink colours are the system's text tokens — so the transform is one
25
+ * implementation here rather than a copy in each consumer, where the copies
26
+ * drift and nobody sees all of them at once.
27
+ *
28
+ * @module
29
+ */
30
+
31
+ import { readFileSync } from "node:fs";
32
+
33
+ /**
34
+ * Iron-gall ink, and cream against a dark ground.
35
+ *
36
+ * These mirror `--sohl-color-text-primary` in the system's
37
+ * `scss/abstracts/_tokens.scss`. Stated once here, so a token change is one
38
+ * edit rather than one per consuming repository.
39
+ *
40
+ * @type {string}
41
+ */
42
+ const INK_LIGHT = "#211d16";
43
+
44
+ /** @type {string} */
45
+ const INK_DARK = "#ece3cf";
46
+
47
+ /**
48
+ * The shapes a theme rule may repaint.
49
+ *
50
+ * Explicitly black, or black by default for want of a `fill`. A shape painted
51
+ * some other colour is deliberate — a white highlight in a two-tone badge — and
52
+ * repainting it would destroy the drawing.
53
+ *
54
+ * @type {string}
55
+ */
56
+ const SELECTOR = [
57
+ '[fill="#000"]',
58
+ '[fill="#000000"]',
59
+ '[fill="black"]',
60
+ "path:not([fill])",
61
+ "rect:not([fill])",
62
+ "circle:not([fill])",
63
+ "ellipse:not([fill])",
64
+ "polygon:not([fill])",
65
+ "polyline:not([fill])",
66
+ "line:not([fill])",
67
+ "g:not([fill])",
68
+ ].join(",");
69
+
70
+ /** @type {string} */
71
+ const STYLE =
72
+ `<style>${SELECTOR}{fill:${INK_LIGHT}}` +
73
+ `@media(prefers-color-scheme:dark){${SELECTOR}{fill:${INK_DARK}}}</style>`;
74
+
75
+ /**
76
+ * Every `style="…"` attribute, capturing its declarations.
77
+ *
78
+ * @type {RegExp}
79
+ */
80
+ const STYLE_ATTR = /style\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
81
+
82
+ /**
83
+ * A `fill` *declaration* — the property itself, at the start or after a `;`.
84
+ *
85
+ * Deliberately not `\bfill\b`, which also matches `fill-rule`, `fill-opacity`
86
+ * and `paint-order: fill`, none of which set a colour.
87
+ *
88
+ * @type {RegExp}
89
+ */
90
+ const FILL_DECL = /(?:^|;)\s*fill\s*:/i;
91
+
92
+ /**
93
+ * Whether any shape sets its fill inline.
94
+ *
95
+ * @param {string} svg - The SVG source.
96
+ * @returns {boolean} Whether an inline `fill` declaration is present.
97
+ */
98
+ function hasInlineFill(svg) {
99
+ for (const [, dq, sq] of svg.matchAll(STYLE_ATTR)) {
100
+ if (FILL_DECL.test(dq ?? sq ?? "")) return true;
101
+ }
102
+ return false;
103
+ }
104
+
105
+ /**
106
+ * The SVG with a scheme-aware fill rule, or unchanged where one cannot apply.
107
+ *
108
+ * Three files are returned as they are. One already carrying a
109
+ * `prefers-color-scheme` rule is themed — by this pass on an earlier run, or by
110
+ * its author — and re-theming it would stack rules. One with an inline `fill`
111
+ * cannot be themed at all, because an inline declaration beats a `<style>` rule
112
+ * and the result would be a half-recoloured icon, which is worse than an
113
+ * unthemed one. One with no `<svg>` element is not an SVG.
114
+ *
115
+ * @param {string} svg - The SVG source.
116
+ * @returns {string} The themed source, or the input unchanged.
117
+ */
118
+ export function injectAdaptiveFill(svg) {
119
+ if (typeof svg !== "string") return svg;
120
+ if (svg.includes("prefers-color-scheme")) return svg;
121
+ if (hasInlineFill(svg)) return svg;
122
+
123
+ const open = svg.match(/<svg\b[^>]*>/i);
124
+ if (!open) return svg;
125
+
126
+ const at = open.index + open[0].length;
127
+ return svg.slice(0, at) + STYLE + svg.slice(at);
128
+ }
129
+
130
+ /**
131
+ * The staging hook: theme an SVG, and pass everything else through untouched.
132
+ *
133
+ * @param {string} sourcePath - The file being staged.
134
+ * @returns {string|null} The themed source, or `null` to stage the file as it
135
+ * is — which is what every non-SVG asset gets.
136
+ */
137
+ export function transform(sourcePath) {
138
+ if (!sourcePath.endsWith(".svg")) return null;
139
+ return injectAdaptiveFill(readFileSync(sourcePath, "utf8"));
140
+ }
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.2",
3
+ "version": "22.1.0",
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
 
@@ -1,3 +1,77 @@
1
+ /**
2
+ * Where a declared `assetTransform` is loaded from.
3
+ *
4
+ * A built-in name resolves to the module this package ships. Anything else is a
5
+ * path, resolved against the repository root the way it always was — so a
6
+ * consumer with a transform of its own is unaffected.
7
+ *
8
+ * @param {string} declared - The authored value.
9
+ * @param {string} rootDir - The repository root.
10
+ * @returns {string} An absolute path to import.
11
+ */
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;
37
+ /**
38
+ * Resolve a package-build configuration from an already-loaded shared one.
39
+ *
40
+ * Separate from {@link loadPackageBuildConfig} because this half is pure: it
41
+ * reads no file and touches no environment, so the validation rules can be
42
+ * described directly by a test instead of through a fixture repository on
43
+ * disk. {@link loadPackageBuildConfig} is the same function with the loading
44
+ * put back.
45
+ *
46
+ * @param {object} shared - The resolved content configuration.
47
+ * @returns {Readonly<PackageBuildConfig>} The frozen configuration.
48
+ * @throws {TypeError} When the reserved section declares something malformed.
49
+ */
50
+ export function resolvePackageBuildConfig(shared: object): Readonly<PackageBuildConfig>;
51
+ /**
52
+ * The repository's resolved package-build configuration.
53
+ *
54
+ * Read on call rather than at import, exactly as content-build resolves its
55
+ * own: importing a module of this package must not require a configuration to
56
+ * exist anywhere above it.
57
+ *
58
+ * @returns {Readonly<PackageBuildConfig>} The frozen configuration.
59
+ * @throws {TypeError} When there is no configuration, or the reserved section
60
+ * declares something malformed.
61
+ */
62
+ export function loadPackageBuildConfig(): Readonly<PackageBuildConfig>;
63
+ /**
64
+ * Manifest keys a repository may **not** declare, because the build derives
65
+ * them and would only overwrite what was written.
66
+ *
67
+ * Silently overwriting is the failure this list exists to prevent: a
68
+ * `version` typed into the configuration would look authoritative, sit there
69
+ * unread, and disagree with the shipped package forever. Declaring one is an
70
+ * error naming the key and where the value actually comes from.
71
+ *
72
+ * @type {Readonly<Record<string, string>>}
73
+ */
74
+ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
1
75
  /**
2
76
  * The resolved `packageBuild` section, every optional half filled in.
3
77
  *
@@ -65,43 +139,16 @@
65
139
  * collections, as collection → source directory.
66
140
  */
67
141
  /**
68
- * Resolve a package-build configuration from an already-loaded shared one.
69
- *
70
- * Separate from {@link loadPackageBuildConfig} because this half is pure: it
71
- * reads no file and touches no environment, so the validation rules can be
72
- * described directly by a test instead of through a fixture repository on
73
- * disk. {@link loadPackageBuildConfig} is the same function with the loading
74
- * put back.
75
- *
76
- * @param {object} shared - The resolved content configuration.
77
- * @returns {Readonly<PackageBuildConfig>} The frozen configuration.
78
- * @throws {TypeError} When the reserved section declares something malformed.
79
- */
80
- export function resolvePackageBuildConfig(shared: object): Readonly<PackageBuildConfig>;
81
- /**
82
- * The repository's resolved package-build configuration.
83
- *
84
- * Read on call rather than at import, exactly as content-build resolves its
85
- * own: importing a module of this package must not require a configuration to
86
- * exist anywhere above it.
87
- *
88
- * @returns {Readonly<PackageBuildConfig>} The frozen configuration.
89
- * @throws {TypeError} When there is no configuration, or the reserved section
90
- * declares something malformed.
91
- */
92
- export function loadPackageBuildConfig(): Readonly<PackageBuildConfig>;
93
- /**
94
- * Manifest keys a repository may **not** declare, because the build derives
95
- * them and would only overwrite what was written.
142
+ * The asset transforms this package ships, by the name a consumer writes.
96
143
  *
97
- * Silently overwriting is the failure this list exists to prevent: a
98
- * `version` typed into the configuration would look authoritative, sit there
99
- * unread, and disagree with the shipped package forever. Declaring one is an
100
- * error naming the key and where the value actually comes from.
144
+ * `packageBuild.assetTransform` takes either one of these names or a path to a
145
+ * module of the consumer's own. A name is the answer where every package wants
146
+ * the same behaviour theming an icon to the reader's colour scheme is not a
147
+ * per-package decision, and a copy in each consumer is a copy that drifts.
101
148
  *
102
149
  * @type {Readonly<Record<string, string>>}
103
150
  */
104
- export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
151
+ export const BUILT_IN_ASSET_TRANSFORMS: Readonly<Record<string, string>>;
105
152
  /**
106
153
  * One staging copy: a source path in the repository, and where it lands under
107
154
  * the staged package root.
@@ -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.
@@ -590,6 +611,22 @@ export type ContentBuildConfigInput = {
590
611
  * package.
591
612
  */
592
613
  foundryPackage?: string | undefined;
614
+ /**
615
+ * `package.json`'s own `homepage` —
616
+ * the site build's `baseURL`. Checked
617
+ * by `checkHomepage` in
618
+ * `config.mjs`.
619
+ */
620
+ homepage?: string | undefined;
621
+ /**
622
+ * `package.json`'s own `author`, in
623
+ * either of npm's forms.
624
+ */
625
+ author?: string | {
626
+ name: string;
627
+ email?: string;
628
+ url?: string;
629
+ } | undefined;
593
630
  /**
594
631
  * Whether the package is a system, a
595
632
  * module, or documentation — the kind
@@ -683,6 +720,22 @@ export type ContentBuildConfig = {
683
720
  * package, which ships no Foundry package.
684
721
  */
685
722
  foundryPackage: string | null;
723
+ /**
724
+ * `package.json`'s own `homepage`,
725
+ * checked by `checkHomepage` in
726
+ * `config.mjs`.
727
+ */
728
+ homepage: string | null;
729
+ /**
730
+ * `package.json`'s own `author`, normalised
731
+ * from either of npm's forms; `null` when
732
+ * the package declares none.
733
+ */
734
+ author: Readonly<{
735
+ name: string;
736
+ email?: string;
737
+ url?: string;
738
+ }> | null;
686
739
  packageKind: PackageKind;
687
740
  /**
688
741
  * Derived, and **conditional**: the served
@@ -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,7 +390,6 @@ 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
@@ -419,9 +398,8 @@ export function resolveOutputRoot(rootDir: string, out: string): string;
419
398
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
420
399
  * wikiErrors: object[], imageErrors: object[], manifests: object|null}}
421
400
  */
422
- export function buildSite({ config, outRoot, sqlTables }?: {
401
+ export function buildSite({ config, sqlTables }?: {
423
402
  config?: object | undefined;
424
- outRoot?: string | undefined;
425
403
  sqlTables?: Map<string, object[]> | undefined;
426
404
  }): {
427
405
  gates: object;