@heroiclands/package-build 23.1.0 → 23.2.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 +71 -0
- package/bin/content-commands.mjs +68 -149
- package/bin/package-build.mjs +56 -10
- package/ci/check-banners.mjs +42 -6
- package/ci/confirm-npm-version.mjs +135 -0
- package/ci/content-format-reference.mjs +14 -5
- package/config.mjs +1 -0
- package/container.mjs +185 -4
- package/content-config.mjs +333 -210
- package/deploy.mjs +116 -10
- package/docs/api.md +238 -246
- package/docs/authoring/assets.md +11 -5
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +87 -0
- package/docs/commands.md +138 -99
- package/docs/configuration.md +277 -125
- package/docs/content-format.md +2 -0
- package/docs/diagnostics.md +48 -13
- package/docs/guides/book.md +341 -21
- package/docs/guides/e2e.md +13 -2
- package/docs/guides/icons.md +14 -4
- package/docs/guides/module-project.md +18 -8
- package/docs/guides/release-and-deploy.md +54 -6
- package/docs/guides/site.md +5 -2
- package/docs/project-setup.md +30 -10
- package/docs/reference/format-details.md +373 -170
- package/docs/reference/note-types.md +128 -109
- package/docs/reference/theme-pages.md +31 -6
- package/e2e.mjs +140 -28
- package/engine/actor-compiler.mjs +22 -31
- package/engine/address-charset.mjs +4 -17
- package/engine/address-fields.mjs +1 -1
- package/engine/address-render.mjs +12 -1
- package/engine/address-values.mjs +6 -0
- package/engine/address.mjs +46 -33
- package/engine/art-fields.mjs +30 -5
- package/engine/art-slots.mjs +10 -1
- package/engine/asset-bindings.mjs +12 -1
- package/engine/base-compiler.mjs +34 -65
- package/engine/being-age.mjs +2 -7
- package/engine/being-measurements.mjs +12 -1
- package/engine/calendar-artifacts.mjs +6 -0
- package/engine/calendar-choice.mjs +12 -1
- package/engine/calendar-format.mjs +12 -1
- package/engine/calendar-human.mjs +12 -1
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-address.mjs +9 -12
- package/engine/content-alerts.mjs +13 -1
- package/engine/content-blocks.mjs +14 -3
- package/engine/content-embeds.mjs +11 -3
- package/engine/content-figures.mjs +9 -0
- package/engine/content-footnotes.mjs +12 -1
- package/engine/content-format-check.mjs +11 -262
- package/engine/content-format.yaml +5 -2
- package/engine/content-images.mjs +3 -0
- package/engine/content-index.mjs +443 -20
- package/engine/content-links.mjs +29 -35
- package/engine/content-lint.mjs +2 -3
- package/engine/content-poetry.mjs +12 -1
- package/engine/content-slug.mjs +2 -14
- package/engine/content-spans.mjs +13 -1
- package/engine/content-tables.mjs +12 -1
- package/engine/culture-choice.mjs +210 -13
- package/engine/data-keys.mjs +19 -16
- package/engine/date-conversion.mjs +12 -1
- package/engine/declaration-check.mjs +9 -0
- package/engine/derived-fields.mjs +6 -7
- package/engine/diagnostics.mjs +34 -4
- package/engine/docs-checks.mjs +9 -0
- package/engine/document-subtypes.mjs +47 -34
- package/engine/event-views.mjs +139 -14
- package/engine/exported-scene.mjs +92 -28
- package/engine/extension-attributes.mjs +12 -1
- package/engine/field-spec.mjs +11 -102
- package/engine/folder-notes.mjs +0 -64
- package/engine/foreign-catalog.mjs +2 -1
- package/engine/format-generated.mjs +9 -0
- package/engine/foundry-entries.mjs +10 -28
- package/engine/frontmatter-lint.mjs +83 -392
- package/engine/frontmatter.mjs +20 -70
- package/engine/generate.mjs +10 -15
- package/engine/heading-attributes.mjs +12 -1
- package/engine/helpers.mjs +41 -102
- package/engine/holdings.mjs +1 -1
- package/engine/homepage.mjs +3 -10
- package/engine/ids.mjs +1 -167
- package/engine/index-records.mjs +6 -4
- package/engine/index.mjs +0 -3
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +13 -14
- package/engine/item-compiler.mjs +4 -10
- package/engine/item-docs.mjs +3 -5
- package/engine/item-registry.mjs +2 -10
- package/engine/journals.mjs +1 -5
- package/engine/literature-notes.mjs +4 -12
- package/engine/macros.mjs +1 -2
- package/engine/map-graphviz.mjs +12 -1
- package/engine/markdown-expressions.mjs +12 -1
- package/engine/markdownlint-run.mjs +567 -0
- package/engine/metadata-index.mjs +42 -12
- package/engine/note-addresses.mjs +143 -35
- package/engine/note-claims.mjs +14 -28
- package/engine/note-dates.mjs +3 -3
- package/engine/note-frontmatter.mjs +6 -0
- package/engine/note-ids.mjs +8 -14
- package/engine/note-package.mjs +8 -25
- package/engine/note-state.mjs +1 -3
- package/engine/note-vocabulary.mjs +148 -84
- package/engine/number-words.mjs +12 -1
- package/engine/pack-config.mjs +15 -25
- package/engine/pack-router.mjs +10 -16
- package/engine/pathnames.mjs +8 -22
- package/engine/pdf-build.mjs +773 -121
- package/engine/pdf-coverage.mjs +188 -0
- package/engine/pdf-index.mjs +54 -7
- package/engine/pdf-pages.mjs +247 -0
- package/engine/pdf-render.mjs +114 -4
- package/engine/pdf-toc.mjs +29 -32
- package/engine/project-init.mjs +46 -6
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +52 -88
- package/engine/readability-lint.mjs +9 -0
- package/engine/reckoning-markers.mjs +12 -1
- package/engine/reference-targets.mjs +7 -0
- package/engine/runtime-only-fields.mjs +5 -8
- package/engine/scene-levels.mjs +2 -2
- package/engine/scenes.mjs +1 -1
- package/engine/shortcode-references.mjs +7 -0
- package/engine/site-build.mjs +30 -16
- package/engine/site-index.mjs +10 -16
- package/engine/site-preview.mjs +6 -0
- package/engine/site-root.mjs +8 -1
- package/engine/social-tie-terms.mjs +12 -1
- package/engine/social-ties.mjs +3 -0
- package/engine/sql-tables.mjs +56 -20
- package/engine/standing-terms.mjs +11 -1
- package/engine/standings.mjs +11 -27
- package/engine/system-block.mjs +39 -141
- package/engine/system-vocabulary.mjs +6 -0
- package/engine/walk-files.mjs +69 -0
- package/engine/web-wikilinks.mjs +23 -3
- package/engine/wikilink-syntax.mjs +32 -21
- package/engine/wikilinks.mjs +27 -16
- package/engine/yaml-lint.mjs +7 -4
- package/githooks/attribution.mjs +8 -0
- package/githooks/commit-message.mjs +9 -0
- package/hm3/actors.mjs +9 -48
- package/hm3/default-item-art.mjs +9 -9
- package/hm3/item-fields.mjs +2 -9
- package/hm3/template-priority.mjs +6 -7
- package/hugo-theme/data/banner-kinds.yaml +78 -0
- package/hugo-theme/data/banners.yaml +2 -3
- package/hugo-theme/layouts/404.html +3 -3
- package/hugo-theme/layouts/partials/banner-url.html +31 -7
- package/hugo-theme/layouts/partials/hero-banner.html +13 -14
- package/hugo-theme/layouts/partials/related.html +1 -1
- package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
- package/hugo-theme/static/css/style.css +1 -1
- package/manifest.mjs +11 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +6 -3
- package/release.mjs +83 -18
- package/sohl/actors.mjs +16 -27
- package/sohl/default-item-art.mjs +3 -3
- package/sohl/document-subtypes.mjs +1 -16
- package/sohl/infobox.mjs +2 -3
- package/sohl/item-builders.mjs +5 -8
- package/sohl/item-fields.mjs +5 -35
- package/sohl/items.mjs +0 -4
- package/sohl/kb-passes.mjs +3 -3
- package/types/container.d.mts +116 -5
- package/types/content-config.d.mts +83 -35
- package/types/deploy.d.mts +42 -6
- package/types/e2e.d.mts +35 -3
- package/types/engine/address-charset.d.mts +4 -17
- package/types/engine/address.d.mts +17 -8
- package/types/engine/art-fields.d.mts +7 -4
- package/types/engine/art-slots.d.mts +6 -0
- package/types/engine/base-compiler.d.mts +15 -17
- package/types/engine/content-address.d.mts +9 -12
- package/types/engine/content-embeds.d.mts +4 -2
- package/types/engine/content-format-check.d.mts +0 -64
- package/types/engine/content-index.d.mts +82 -3
- package/types/engine/content-links.d.mts +2 -4
- package/types/engine/culture-choice.d.mts +51 -8
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/derived-fields.d.mts +1 -2
- package/types/engine/diagnostics.d.mts +27 -4
- package/types/engine/document-subtypes.d.mts +12 -31
- package/types/engine/event-views.d.mts +14 -2
- package/types/engine/exported-scene.d.mts +3 -2
- package/types/engine/field-spec.d.mts +13 -122
- package/types/engine/folder-notes.d.mts +0 -39
- package/types/engine/foreign-catalog.d.mts +2 -1
- package/types/engine/foundry-entries.d.mts +4 -5
- package/types/engine/frontmatter-lint.d.mts +2 -2
- package/types/engine/frontmatter.d.mts +19 -45
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +25 -42
- package/types/engine/homepage.d.mts +3 -10
- package/types/engine/ids.d.mts +0 -114
- package/types/engine/index-records.d.mts +6 -4
- package/types/engine/index.d.mts +0 -1
- package/types/engine/infobox-render.d.mts +7 -1
- package/types/engine/infobox.d.mts +3 -2
- package/types/engine/item-docs.d.mts +1 -1
- package/types/engine/item-registry.d.mts +2 -4
- package/types/engine/literature-notes.d.mts +1 -1
- package/types/engine/markdownlint-run.d.mts +40 -0
- package/types/engine/metadata-index.d.mts +9 -1
- package/types/engine/note-addresses.d.mts +27 -2
- package/types/engine/note-claims.d.mts +3 -5
- package/types/engine/note-ids.d.mts +2 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-state.d.mts +1 -3
- package/types/engine/note-vocabulary.d.mts +28 -21
- package/types/engine/pack-config.d.mts +2 -6
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/pathnames.d.mts +3 -3
- package/types/engine/pdf-build.d.mts +98 -12
- package/types/engine/pdf-coverage.d.mts +60 -0
- package/types/engine/pdf-index.d.mts +34 -5
- package/types/engine/pdf-pages.d.mts +143 -0
- package/types/engine/pdf-render.d.mts +45 -1
- package/types/engine/pdf-toc.d.mts +12 -14
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +11 -10
- package/types/engine/runtime-only-fields.d.mts +2 -2
- package/types/engine/site-build.d.mts +9 -5
- package/types/engine/site-index.d.mts +4 -5
- package/types/engine/site-root.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -2
- package/types/engine/standing-terms.d.mts +2 -1
- package/types/engine/standings.d.mts +7 -9
- package/types/engine/system-block.d.mts +10 -44
- package/types/engine/walk-files.d.mts +15 -0
- package/types/engine/wikilink-syntax.d.mts +18 -19
- package/types/engine/yaml-lint.d.mts +5 -4
- package/types/hm3/actors.d.mts +1 -2
- package/types/hm3/item-fields.d.mts +2 -4
- package/types/hm3/template-priority.d.mts +6 -7
- package/types/manifest.d.mts +3 -2
- package/types/release.d.mts +12 -7
- package/types/sohl/actors.d.mts +2 -1
- package/types/sohl/document-subtypes.d.mts +1 -2
- package/types/sohl/item-fields.d.mts +3 -7
- package/engine/retired-fields.mjs +0 -631
- package/types/engine/retired-fields.d.mts +0 -354
package/ci/check-banners.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* This file is part of the Heroic Lands
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
3
|
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
4
|
*
|
|
5
5
|
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
6
|
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
7
|
*
|
|
8
|
-
* For full terms, see the LICENSE file in the project root or visit:
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
9
|
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
10
|
*
|
|
11
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
* So this fetches every declared name and fails on any non-200. It is the test
|
|
26
26
|
* the template cannot make, moved to the one place that can make it.
|
|
27
27
|
*
|
|
28
|
+
* It also refuses a name in `data/banner-kinds.yaml` that `banners.yaml` does
|
|
29
|
+
* not declare, before any request is made: a mapping to undrawn art is a dead
|
|
30
|
+
* URL on every page of that kind.
|
|
31
|
+
*
|
|
28
32
|
* Usage:
|
|
29
33
|
* node utils/check-banners.mjs [--base <url>] [--concurrency <n>]
|
|
30
34
|
*
|
|
@@ -43,6 +47,7 @@ import { parse } from "yaml";
|
|
|
43
47
|
|
|
44
48
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
49
|
const DATA_FILE = resolve(HERE, "..", "hugo-theme", "data", "banners.yaml");
|
|
50
|
+
const KINDS_FILE = resolve(HERE, "..", "hugo-theme", "data", "banner-kinds.yaml");
|
|
46
51
|
const DEFAULT_BASE = "https://cdn.heroiclands.org";
|
|
47
52
|
|
|
48
53
|
function parseArgs(argv) {
|
|
@@ -71,13 +76,13 @@ function parseArgs(argv) {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/** Diagnostics carry the fields they can know and drop the ones they cannot. */
|
|
74
|
-
function diag(line, message) {
|
|
75
|
-
const where = line ? `${
|
|
79
|
+
function diag(line, message, file = DATA_FILE) {
|
|
80
|
+
const where = line ? `${file}:${line}` : file;
|
|
76
81
|
console.error(`${where}: error: ${message}`);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
function fail(message, line) {
|
|
80
|
-
diag(line, message);
|
|
84
|
+
function fail(message, line, file) {
|
|
85
|
+
diag(line, message, file);
|
|
81
86
|
process.exit(1);
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -123,6 +128,36 @@ function readInventory() {
|
|
|
123
128
|
return { source, dir, available };
|
|
124
129
|
}
|
|
125
130
|
|
|
131
|
+
/** Every banner name `banner-kinds.yaml` maps a page kind to, with the kind that maps it. */
|
|
132
|
+
function mappedNames() {
|
|
133
|
+
const source = readFileSync(KINDS_FILE, "utf8");
|
|
134
|
+
const kinds = parse(source)?.kinds;
|
|
135
|
+
if (!kinds || typeof kinds !== "object") fail("`kinds` must be a map", 0, KINDS_FILE);
|
|
136
|
+
const mapped = [];
|
|
137
|
+
for (const [type, value] of Object.entries(kinds)) {
|
|
138
|
+
if (typeof value === "string") mapped.push({ kind: type, name: value });
|
|
139
|
+
else
|
|
140
|
+
for (const [subType, name] of Object.entries(value ?? {}))
|
|
141
|
+
mapped.push({ kind: `${type}/${subType}`, name });
|
|
142
|
+
}
|
|
143
|
+
return { source, mapped };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Fail on a mapped name the inventory does not declare. */
|
|
147
|
+
function checkMapping(available) {
|
|
148
|
+
const { source, mapped } = mappedNames();
|
|
149
|
+
const undeclared = mapped.filter(({ name }) => !available.includes(name));
|
|
150
|
+
for (const { kind, name } of undeclared) {
|
|
151
|
+
const line = source.split("\n").findIndex((l) => l.trim().endsWith(`: ${name}`));
|
|
152
|
+
diag(
|
|
153
|
+
line === -1 ? 0 : line + 1,
|
|
154
|
+
`${kind} maps to banner ${name}, which banners.yaml does not declare`,
|
|
155
|
+
KINDS_FILE,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (undeclared.length) process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
126
161
|
async function probe(url) {
|
|
127
162
|
try {
|
|
128
163
|
// HEAD first — it is what the question actually is. Some object stores
|
|
@@ -147,6 +182,7 @@ async function probe(url) {
|
|
|
147
182
|
async function main() {
|
|
148
183
|
const { base, concurrency } = parseArgs(process.argv.slice(2));
|
|
149
184
|
const { source, dir, available } = readInventory();
|
|
185
|
+
checkMapping(available);
|
|
150
186
|
const root = `${base.replace(/\/$/, "")}/${dir.replace(/^\//, "")}`;
|
|
151
187
|
|
|
152
188
|
const failures = [];
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
* Asserts that npm serves an exact package version.
|
|
16
|
+
*
|
|
17
|
+
* `npm publish` returns before the registry serves the new version, and a
|
|
18
|
+
* version that is accepted but never served is indistinguishable from success
|
|
19
|
+
* by the publish alone. This polls `npm view <name>@<version> version` until
|
|
20
|
+
* it prints the version, then exits 0. When the time budget runs out it exits 1
|
|
21
|
+
* with an `::error::` annotation naming the package, the version and the last
|
|
22
|
+
* registry answer.
|
|
23
|
+
*
|
|
24
|
+
* Usage: `node ci/confirm-npm-version.mjs <name> <version> [--interval <s>]
|
|
25
|
+
* [--timeout <s>]`. The defaults are 15 seconds between queries and 600 seconds
|
|
26
|
+
* overall.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { spawnSync } from "node:child_process";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
/** Seconds between registry queries. */
|
|
33
|
+
export const DEFAULT_INTERVAL_SECONDS = 15;
|
|
34
|
+
/** Seconds the registry has to serve the version. */
|
|
35
|
+
export const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ask the registry which version `name@version` resolves to.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} spec - `<name>@<version>`.
|
|
41
|
+
* @returns {string} The version npm printed, or its error text when the query
|
|
42
|
+
* fails (an unpublished version answers E404).
|
|
43
|
+
*/
|
|
44
|
+
export function queryNpm(spec) {
|
|
45
|
+
const result = spawnSync("npm", ["view", spec, "version"], { encoding: "utf8" });
|
|
46
|
+
if (result.error) return result.error.message;
|
|
47
|
+
const out = (result.stdout ?? "").trim();
|
|
48
|
+
if (result.status === 0) return out;
|
|
49
|
+
const err = (result.stderr ?? "").trim().replace(/\s*\n\s*/g, " | ");
|
|
50
|
+
return err || out || `npm exited ${result.status}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Poll until the registry serves `version` of `name`.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} options
|
|
57
|
+
* @param {string} options.name - Package name.
|
|
58
|
+
* @param {string} options.version - Exact version expected.
|
|
59
|
+
* @param {(spec: string) => string} [options.query] - Registry query; returns
|
|
60
|
+
* the registry's answer for `<name>@<version>`.
|
|
61
|
+
* @param {(ms: number) => Promise<void>} [options.sleep] - Waits between queries.
|
|
62
|
+
* @param {number} [options.intervalSeconds] - Seconds between queries.
|
|
63
|
+
* @param {number} [options.timeoutSeconds] - Seconds before giving up.
|
|
64
|
+
* @param {(line: string) => void} [options.log] - Progress output.
|
|
65
|
+
* @returns {Promise<{ok: true, attempts: number} | {ok: false, attempts: number, last: string}>}
|
|
66
|
+
*/
|
|
67
|
+
export async function confirmNpmVersion({
|
|
68
|
+
name,
|
|
69
|
+
version,
|
|
70
|
+
query = queryNpm,
|
|
71
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
72
|
+
intervalSeconds = DEFAULT_INTERVAL_SECONDS,
|
|
73
|
+
timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
|
|
74
|
+
log = () => {},
|
|
75
|
+
}) {
|
|
76
|
+
const spec = `${name}@${version}`;
|
|
77
|
+
const attemptLimit = Math.floor(timeoutSeconds / intervalSeconds) + 1;
|
|
78
|
+
let last = "";
|
|
79
|
+
for (let attempts = 1; attempts <= attemptLimit; attempts++) {
|
|
80
|
+
last = query(spec);
|
|
81
|
+
if (last === version) {
|
|
82
|
+
log(`${spec} is served by npm.`);
|
|
83
|
+
return { ok: true, attempts };
|
|
84
|
+
}
|
|
85
|
+
log(`${spec} is not served yet (${last || "empty answer"}).`);
|
|
86
|
+
if (attempts < attemptLimit) await sleep(intervalSeconds * 1000);
|
|
87
|
+
}
|
|
88
|
+
return { ok: false, attempts: attemptLimit, last };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseArguments(argv) {
|
|
92
|
+
const positional = [];
|
|
93
|
+
const options = {};
|
|
94
|
+
for (let i = 0; i < argv.length; i++) {
|
|
95
|
+
const arg = argv[i];
|
|
96
|
+
if (arg === "--interval" || arg === "--timeout") {
|
|
97
|
+
const value = Number(argv[++i]);
|
|
98
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
99
|
+
throw new Error(`${arg} takes a positive number of seconds.`);
|
|
100
|
+
}
|
|
101
|
+
options[arg === "--interval" ? "intervalSeconds" : "timeoutSeconds"] = value;
|
|
102
|
+
} else if (arg.startsWith("--")) {
|
|
103
|
+
throw new Error(`Unknown option ${arg}.`);
|
|
104
|
+
} else {
|
|
105
|
+
positional.push(arg);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (positional.length !== 2) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Usage: confirm-npm-version.mjs <name> <version> [--interval <s>] [--timeout <s>]",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return { name: positional[0], version: positional[1], ...options };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main(argv) {
|
|
117
|
+
let options;
|
|
118
|
+
try {
|
|
119
|
+
options = parseArguments(argv);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`::error::${error.message}`);
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
const outcome = await confirmNpmVersion({ ...options, log: (line) => console.log(line) });
|
|
125
|
+
if (outcome.ok) return 0;
|
|
126
|
+
const timeout = options.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
127
|
+
console.error(
|
|
128
|
+
`::error::npm does not serve ${options.name}@${options.version} after ${timeout}s and ${outcome.attempts} queries. Last registry answer: ${outcome.last || "(empty)"}`,
|
|
129
|
+
);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
134
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
135
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
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
|
+
*
|
|
2
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
12
|
*/
|
|
4
13
|
|
|
@@ -12,6 +21,7 @@ import {
|
|
|
12
21
|
NOTE_TOP_LEVEL_FIELDS,
|
|
13
22
|
NOTE_VOCABULARY,
|
|
14
23
|
SHARED_DATA_FIELDS,
|
|
24
|
+
dataFields,
|
|
15
25
|
} from "../engine/note-vocabulary.mjs";
|
|
16
26
|
import { EXPRESSION_HELPERS } from "../engine/markdown-expressions.mjs";
|
|
17
27
|
import { PAGE_LIST_ATTRIBUTES } from "../engine/page-lists.mjs";
|
|
@@ -98,7 +108,7 @@ const rows = [
|
|
|
98
108
|
"",
|
|
99
109
|
"## Shared `data` fields",
|
|
100
110
|
"",
|
|
101
|
-
"These fields are accepted by every note type. A field's value can still be irrelevant to a particular output; the build reports that where it can.",
|
|
111
|
+
"These fields are accepted by every note type. A field's value can still be irrelevant to a particular output; the build reports that where it can. `data.icon` is not shared: it is listed under each type whose document carries art.",
|
|
102
112
|
"",
|
|
103
113
|
"| Field | Shape | Meaning |",
|
|
104
114
|
"| --- | --- | --- |",
|
|
@@ -127,13 +137,12 @@ for (const [type, spec] of contract.types) {
|
|
|
127
137
|
);
|
|
128
138
|
}
|
|
129
139
|
rows.push("| Field | Shape | Meaning |", "| --- | --- | --- |");
|
|
140
|
+
const typeData = dataFields(type).filter((field) => !SHARED_DATA_FIELDS.includes(field));
|
|
130
141
|
rows.push(
|
|
131
|
-
...(
|
|
132
|
-
fields(declared.data)
|
|
133
|
-
: ["| — | — | No type-specific `data` fields. |"]),
|
|
142
|
+
...(typeData.length ? fields(typeData) : ["| — | — | No type-specific `data` fields. |"]),
|
|
134
143
|
);
|
|
135
144
|
rows.push("");
|
|
136
|
-
rows.push(...innerKeyTable(
|
|
145
|
+
rows.push(...innerKeyTable(typeData));
|
|
137
146
|
const claims = contract.claims.filter((claim) => !claim.shared && claim.noteType === type);
|
|
138
147
|
if (claims.length) {
|
|
139
148
|
rows.push("**System mappings**", "", "| Source | System | Target |", "| --- | --- | --- |");
|
package/config.mjs
CHANGED
|
@@ -94,6 +94,7 @@ const SECTION_KEYS = [
|
|
|
94
94
|
export const DERIVED_MANIFEST_KEYS = Object.freeze({
|
|
95
95
|
id: "`foundryPackage`, itself derived from package.json `name`",
|
|
96
96
|
version: "package.json `version`",
|
|
97
|
+
authors: "the `authors` list at the top level of package-build.config.yaml",
|
|
97
98
|
description: "`packageBuild.manifest.descriptionHtml`",
|
|
98
99
|
url: "package.json `repository`",
|
|
99
100
|
bugs: "package.json `repository`",
|
package/container.mjs
CHANGED
|
@@ -217,10 +217,12 @@ export function resolveFoundryVersion(
|
|
|
217
217
|
/**
|
|
218
218
|
* The image a run uses.
|
|
219
219
|
*
|
|
220
|
-
* `FOUNDRYVTT_CONTAINER_IMAGE` wins, then `packageBuild.container.image
|
|
221
|
-
* felddy's
|
|
222
|
-
*
|
|
223
|
-
*
|
|
220
|
+
* `FOUNDRYVTT_CONTAINER_IMAGE` wins, then `packageBuild.container.image`. A
|
|
221
|
+
* pinned exact build takes felddy's tag for that build: each image installs its
|
|
222
|
+
* own build by default and warns that a different `FOUNDRY_VERSION` "may not
|
|
223
|
+
* function properly". Otherwise felddy's major tag for whichever version the
|
|
224
|
+
* package implies — the pin, or failing that the compatibility floor. Only a
|
|
225
|
+
* package that claims neither floats.
|
|
224
226
|
*
|
|
225
227
|
* @param {object} [opts]
|
|
226
228
|
* @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
|
|
@@ -238,6 +240,7 @@ export function resolveImage({
|
|
|
238
240
|
const explicit = env.FOUNDRYVTT_CONTAINER_IMAGE?.trim();
|
|
239
241
|
if (explicit) return explicit;
|
|
240
242
|
if (image) return image;
|
|
243
|
+
if (EXACT_BUILD.test(version ?? "")) return `felddy/foundryvtt:${version}`;
|
|
241
244
|
const major = (version ?? compatibilityMinimum ?? "").split(".")[0];
|
|
242
245
|
return major ? `felddy/foundryvtt:${major}` : FLOATING_IMAGE;
|
|
243
246
|
}
|
|
@@ -331,6 +334,52 @@ export function passthroughEnv(env = process.env) {
|
|
|
331
334
|
return pairs;
|
|
332
335
|
}
|
|
333
336
|
|
|
337
|
+
/** The account variables the image signs in to foundryvtt.com with. */
|
|
338
|
+
const ACCOUNT_VARIABLES = Object.freeze(["FOUNDRY_USERNAME", "FOUNDRY_PASSWORD"]);
|
|
339
|
+
|
|
340
|
+
/** The shortest value the image reads as a licence key rather than an index. */
|
|
341
|
+
const LICENSE_KEY_MIN_LENGTH = 24;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Whether the image has to sign in to foundryvtt.com to start.
|
|
345
|
+
*
|
|
346
|
+
* Given `FOUNDRY_USERNAME` and `FOUNDRY_PASSWORD`, the image signs in and asks
|
|
347
|
+
* for a download URL on every container it creates, even when the release is
|
|
348
|
+
* already in its cache. Each of those is a request foundryvtt.com rate-limits,
|
|
349
|
+
* and one refused with HTTP 429 stops the container before Foundry starts. The
|
|
350
|
+
* account serves two purposes only: downloading a release that is not cached,
|
|
351
|
+
* and fetching a licence key for an unlicensed data root. When neither applies
|
|
352
|
+
* the account is withheld, and the release installs from the cache with no
|
|
353
|
+
* network request.
|
|
354
|
+
*
|
|
355
|
+
* The image caches `foundryvtt-<version>.zip` in the mounted cache, or in
|
|
356
|
+
* `container_cache` beneath the data root when no cache is mounted.
|
|
357
|
+
*
|
|
358
|
+
* @param {object} opts
|
|
359
|
+
* @param {string|null} [opts.cacheDir] - The host cache mounted into the container.
|
|
360
|
+
* @param {string} opts.dataRoot - The host data root mounted at `/data`.
|
|
361
|
+
* @param {string|null} [opts.version] - The pinned build; with none, the
|
|
362
|
+
* archive's name is unknown.
|
|
363
|
+
* @param {string|null} [opts.licenseKey] - The licence key the image is given.
|
|
364
|
+
* @param {(file: string) => boolean} [opts.fileExists] - Replaceable for tests.
|
|
365
|
+
* @returns {boolean} Whether to pass the account through.
|
|
366
|
+
*/
|
|
367
|
+
export function needsFoundryAccount({
|
|
368
|
+
cacheDir = null,
|
|
369
|
+
dataRoot,
|
|
370
|
+
version = null,
|
|
371
|
+
licenseKey = null,
|
|
372
|
+
fileExists = (file) => fs.existsSync(file),
|
|
373
|
+
}) {
|
|
374
|
+
if (!version) return true;
|
|
375
|
+
const cache = cacheDir ?? path.join(dataRoot, "container_cache");
|
|
376
|
+
if (!fileExists(path.join(cache, `foundryvtt-${version}.zip`))) return true;
|
|
377
|
+
const licensed =
|
|
378
|
+
(licenseKey?.length ?? 0) >= LICENSE_KEY_MIN_LENGTH ||
|
|
379
|
+
fileExists(path.join(dataRoot, "Config", "license.json"));
|
|
380
|
+
return !licensed;
|
|
381
|
+
}
|
|
382
|
+
|
|
334
383
|
/**
|
|
335
384
|
* The full `docker run` argument vector for a stage's container.
|
|
336
385
|
*
|
|
@@ -348,6 +397,13 @@ export function passthroughEnv(env = process.env) {
|
|
|
348
397
|
* @param {string|null} [opts.version] - Exact build to pin.
|
|
349
398
|
* @param {string|null} [opts.world] - World to auto-launch; `""` forces none.
|
|
350
399
|
* @param {string|null} [opts.licenseKey] - Dedicated licence key.
|
|
400
|
+
* @param {boolean} [opts.account] - Whether to pass `FOUNDRY_USERNAME` and
|
|
401
|
+
* `FOUNDRY_PASSWORD` through; see {@link needsFoundryAccount}.
|
|
402
|
+
* @param {string|null} [opts.user] - `--user` value to run the container as.
|
|
403
|
+
* `felddy/foundryvtt` runs Foundry as user 1000 and ignores `FOUNDRY_UID` and
|
|
404
|
+
* `FOUNDRY_GID`; under rootless Docker, container root is the host user, so
|
|
405
|
+
* `0:0` makes every file under the data root theirs. Blank or `null` leaves
|
|
406
|
+
* the image's own user.
|
|
351
407
|
* @returns {string[]} Arguments after `docker`.
|
|
352
408
|
*/
|
|
353
409
|
export function dockerRunArgs({
|
|
@@ -360,6 +416,8 @@ export function dockerRunArgs({
|
|
|
360
416
|
version = null,
|
|
361
417
|
world = null,
|
|
362
418
|
licenseKey = null,
|
|
419
|
+
account = true,
|
|
420
|
+
user = null,
|
|
363
421
|
}) {
|
|
364
422
|
const args = [
|
|
365
423
|
"run",
|
|
@@ -380,11 +438,13 @@ export function dockerRunArgs({
|
|
|
380
438
|
args.push("--volume", `${cacheDir}:${CACHE_MOUNT}`, "-e", `CONTAINER_CACHE=${CACHE_MOUNT}`);
|
|
381
439
|
}
|
|
382
440
|
for (const [key, value] of passthroughEnv(env)) {
|
|
441
|
+
if (!account && ACCOUNT_VARIABLES.includes(key)) continue;
|
|
383
442
|
args.push("-e", `${key}=${value}`);
|
|
384
443
|
}
|
|
385
444
|
if (version) args.push("-e", `FOUNDRY_VERSION=${version}`);
|
|
386
445
|
if (world !== null) args.push("-e", `FOUNDRY_WORLD=${world}`);
|
|
387
446
|
if (licenseKey) args.push("-e", `FOUNDRY_LICENSE_KEY=${licenseKey}`);
|
|
447
|
+
if (user?.trim()) args.push("--user", user.trim());
|
|
388
448
|
args.push(image);
|
|
389
449
|
return args;
|
|
390
450
|
}
|
|
@@ -410,6 +470,118 @@ export function runDocker(args) {
|
|
|
410
470
|
return result.status ?? 0;
|
|
411
471
|
}
|
|
412
472
|
|
|
473
|
+
/**
|
|
474
|
+
* The `docker run` argument vector for a short-lived container that runs a
|
|
475
|
+
* shell script against a stage's data root.
|
|
476
|
+
*
|
|
477
|
+
* It uses the stage's own image and mounts the data root at `/data`, so the
|
|
478
|
+
* script sees files exactly as the Foundry container does. Under rootless
|
|
479
|
+
* Docker, only the container's user namespace can unlink what the container's
|
|
480
|
+
* user wrote, and only it can hand files to that user.
|
|
481
|
+
*
|
|
482
|
+
* @param {object} opts
|
|
483
|
+
* @param {string} opts.image - Image reference.
|
|
484
|
+
* @param {string} opts.dataRoot - Host directory to mount at `/data`.
|
|
485
|
+
* @param {string} opts.script - A `sh` script; `$1`, `$2` … are `args`.
|
|
486
|
+
* @param {string[]} [opts.args] - Positional arguments for the script.
|
|
487
|
+
* @param {string|null} [opts.user] - `--user` value; `null` runs as the image's
|
|
488
|
+
* own user.
|
|
489
|
+
* @returns {string[]} Arguments after `docker`.
|
|
490
|
+
*/
|
|
491
|
+
export function dataRootScriptArgs({ image, dataRoot, script, args = [], user = null }) {
|
|
492
|
+
const argv = [
|
|
493
|
+
"run",
|
|
494
|
+
"--rm",
|
|
495
|
+
"--volume",
|
|
496
|
+
`${path.resolve(dataRoot)}:/data`,
|
|
497
|
+
"--entrypoint",
|
|
498
|
+
"sh",
|
|
499
|
+
];
|
|
500
|
+
if (user?.trim()) argv.push("--user", user.trim());
|
|
501
|
+
argv.push(image, "-c", script, "sh", ...args);
|
|
502
|
+
return argv;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Run a shell script against a stage's data root in a throwaway container.
|
|
507
|
+
*
|
|
508
|
+
* @param {object} opts - As for {@link dataRootScriptArgs}.
|
|
509
|
+
* @returns {string} The script's standard output.
|
|
510
|
+
* @throws {Error} When `docker` is missing or the script exits non-zero.
|
|
511
|
+
*/
|
|
512
|
+
export function runDataRootScript(opts) {
|
|
513
|
+
const result = spawnSync("docker", dataRootScriptArgs(opts), { encoding: "utf8" });
|
|
514
|
+
if (result.error?.code === "ENOENT") {
|
|
515
|
+
throw new Error(
|
|
516
|
+
"docker was not found on PATH. Install Docker and make sure the `docker` CLI is available.",
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (result.status !== 0) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`A helper container failed (exit ${result.status}): ` +
|
|
522
|
+
`${(result.stderr || result.stdout || "").trim()}`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return result.stdout ?? "";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* A tree's path beneath the data root, as the helper container sees it there.
|
|
530
|
+
*
|
|
531
|
+
* @param {string} dataRoot - Host directory mounted at `/data`.
|
|
532
|
+
* @param {string} tree - A host path beneath it.
|
|
533
|
+
* @returns {string} The `/`-separated path relative to the data root.
|
|
534
|
+
* @throws {Error} When the tree is the data root or lies outside it.
|
|
535
|
+
*/
|
|
536
|
+
function beneathDataRoot(dataRoot, tree) {
|
|
537
|
+
const relative = path.relative(path.resolve(dataRoot), path.resolve(tree));
|
|
538
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
539
|
+
throw new Error(`${tree} is not beneath the data root ${dataRoot}.`);
|
|
540
|
+
}
|
|
541
|
+
return relative.split(path.sep).join("/");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* A remover for trees under a stage's data root that the container's user
|
|
546
|
+
* owns, run as root in a throwaway container on that data root — the same
|
|
547
|
+
* mechanism that hands an end-to-end install to the container's user.
|
|
548
|
+
*
|
|
549
|
+
* @param {object} opts
|
|
550
|
+
* @param {ResolvedContainer} opts.container - The stage's container.
|
|
551
|
+
* @param {(opts: object) => string} [opts.runScript] - Throwaway-container runner.
|
|
552
|
+
* @returns {(tree: string, where: {dataRoot: string}) => void} Removes one tree.
|
|
553
|
+
*/
|
|
554
|
+
export function containerTreeRemover({ container, runScript = runDataRootScript }) {
|
|
555
|
+
return (tree, { dataRoot }) => {
|
|
556
|
+
const relative = beneathDataRoot(dataRoot, tree);
|
|
557
|
+
runScript({
|
|
558
|
+
image: container.image,
|
|
559
|
+
dataRoot,
|
|
560
|
+
user: "0:0",
|
|
561
|
+
script: 'rm -rf "/data/$1"',
|
|
562
|
+
args: [relative],
|
|
563
|
+
});
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* The command {@link containerTreeRemover} runs, written out for a person to
|
|
569
|
+
* paste when the deploy could not run it.
|
|
570
|
+
*
|
|
571
|
+
* @param {object} opts
|
|
572
|
+
* @param {string} opts.image - Image reference.
|
|
573
|
+
* @param {string} opts.dataRoot - Host directory mounted at `/data`.
|
|
574
|
+
* @param {string} opts.tree - The tree to remove, beneath the data root.
|
|
575
|
+
* @returns {string} A `docker run` command line.
|
|
576
|
+
*/
|
|
577
|
+
export function containerRemovalCommand({ image, dataRoot, tree }) {
|
|
578
|
+
const relative = beneathDataRoot(dataRoot, tree);
|
|
579
|
+
return (
|
|
580
|
+
`docker run --rm --user 0:0 --volume ${path.resolve(dataRoot)}:/data ` +
|
|
581
|
+
`--entrypoint rm ${image} -rf /data/${relative}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
413
585
|
/**
|
|
414
586
|
* Run the `docker` CLI and capture its output, tolerating failure.
|
|
415
587
|
*
|
|
@@ -487,6 +659,7 @@ export function clearStaleLock(dataRoot, log = () => {}) {
|
|
|
487
659
|
* @property {string|null} world
|
|
488
660
|
* @property {string|null} licenseKey
|
|
489
661
|
* @property {string|null} cacheDir
|
|
662
|
+
* @property {string|null} user - From `FOUNDRYVTT_CONTAINER_USER`.
|
|
490
663
|
*/
|
|
491
664
|
|
|
492
665
|
/**
|
|
@@ -522,6 +695,7 @@ export function resolveContainer({ stage, config, env = process.env }) {
|
|
|
522
695
|
world: resolveWorld(stage, { env, stages }),
|
|
523
696
|
licenseKey: resolveLicenseKey(stage, env),
|
|
524
697
|
cacheDir: env.FOUNDRYVTT_CACHE?.trim() || null,
|
|
698
|
+
user: env.FOUNDRYVTT_CONTAINER_USER?.trim() || null,
|
|
525
699
|
};
|
|
526
700
|
}
|
|
527
701
|
|
|
@@ -563,6 +737,13 @@ export function startContainer(container, { dataRoot, env = process.env, log = (
|
|
|
563
737
|
version: container.version,
|
|
564
738
|
world: container.world,
|
|
565
739
|
licenseKey: container.licenseKey,
|
|
740
|
+
account: needsFoundryAccount({
|
|
741
|
+
cacheDir: container.cacheDir,
|
|
742
|
+
dataRoot,
|
|
743
|
+
version: container.version,
|
|
744
|
+
licenseKey: container.licenseKey ?? env.FOUNDRY_LICENSE_KEY?.trim() ?? null,
|
|
745
|
+
}),
|
|
746
|
+
user: container.user,
|
|
566
747
|
}),
|
|
567
748
|
);
|
|
568
749
|
if (status === 0) {
|