@heroiclands/package-build 22.0.2 → 22.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/CONTENT.md +34 -18
- package/README.md +1 -1
- package/bin/content-build.mjs +40 -22
- package/bin/package-build.mjs +34 -0
- package/config.mjs +88 -3
- package/content-config.mjs +246 -12
- package/docs/api.md +10 -7
- package/docs/commands.md +122 -32
- package/docs/configuration.md +257 -62
- package/docs/getting-started.md +15 -4
- package/docs/project-setup.md +45 -20
- package/engine/content-index.mjs +25 -6
- package/engine/pack-config.mjs +21 -2
- package/engine/site-build.mjs +15 -52
- package/engine/site-config.mjs +462 -0
- package/engine/site-root.mjs +165 -0
- package/engine/svg-theme.mjs +140 -0
- package/manifest.mjs +12 -7
- package/package.json +2 -1
- package/stage.mjs +4 -3
- package/types/config.d.mts +80 -33
- package/types/content-config.d.mts +53 -0
- package/types/engine/site-build.d.mts +3 -25
- package/types/engine/site-config.d.mts +223 -0
- package/types/engine/site-root.d.mts +87 -0
- package/types/engine/svg-theme.d.mts +22 -0
- package/types/manifest.d.mts +4 -4
- package/types/stage.d.mts +4 -3
|
@@ -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
|
|
32
|
-
* compatibility ranges and the pack list. Declaring one of
|
|
33
|
-
* rather than an override: the authored copy would be
|
|
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
|
|
463
|
-
* and system compatibility ranges, and the pack list.
|
|
464
|
-
* also declared: an authored copy would be overwritten
|
|
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
|
+
"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
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
package/types/config.d.mts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
|
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
|
|
109
|
-
* one level above the
|
|
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,
|
|
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;
|