@heroiclands/package-build 23.4.1 → 23.5.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 +27 -0
- package/README.md +29 -25
- package/assets/images/provenance.yaml +1 -0
- package/bin/package-build.mjs +259 -28
- package/ci/prettier.mjs +73 -0
- package/config.mjs +10 -0
- package/container.mjs +232 -1
- package/content-config.mjs +5 -1
- package/docs/api.md +100 -52
- package/docs/authoring/assets.md +16 -1
- package/docs/commands.md +154 -7
- package/docs/configuration.md +39 -2
- package/docs/content-format.md +30 -6
- package/docs/guides/e2e.md +38 -0
- package/docs/guides/icons.md +5 -0
- package/docs/guides/release-and-deploy.md +36 -1
- package/docs/reference/format-details.md +180 -34
- package/docs/reference/note-types.md +69 -69
- package/engine/asset-index.mjs +63 -22
- package/engine/asset-references.mjs +355 -0
- package/engine/asset-shipping.mjs +363 -0
- package/engine/asset-types.mjs +70 -0
- package/engine/base-compiler.mjs +24 -1
- package/engine/cdn-publish.mjs +618 -0
- package/engine/index.mjs +6 -0
- package/engine/note-vocabulary.mjs +4 -1
- package/engine/pathnames.mjs +21 -10
- package/engine/rank-ladder.mjs +40 -16
- package/engine/scenes.mjs +1 -1
- package/engine/standing-terms.mjs +3 -1
- package/package.json +4 -3
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +69 -7
- package/sohl/actors.mjs +12 -4
- package/sohl/default-item-art.mjs +23 -19
- package/types/config.d.mts +7 -0
- package/types/container.d.mts +137 -1
- package/types/engine/asset-index.d.mts +3 -2
- package/types/engine/asset-references.d.mts +136 -0
- package/types/engine/asset-shipping.d.mts +240 -0
- package/types/engine/asset-types.d.mts +36 -0
- package/types/engine/base-compiler.d.mts +7 -1
- package/types/engine/cdn-publish.d.mts +311 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/pathnames.d.mts +2 -1
- package/types/engine/rank-ladder.d.mts +25 -6
- package/types/release.d.mts +5 -3
- package/types/sohl/default-item-art.d.mts +2 -1
package/engine/pathnames.mjs
CHANGED
|
@@ -36,15 +36,23 @@
|
|
|
36
36
|
* being built, and the whole of it is the suffix. That is the ordinary case and
|
|
37
37
|
* the one nearly every note writes.
|
|
38
38
|
*
|
|
39
|
-
* The four forms, for a `harnensemble` note writing `images/map.webp`
|
|
39
|
+
* The four forms, for a `harnensemble` note writing `images/places/map.webp`
|
|
40
40
|
* (`harnensemble` ships as the Foundry module `harn-ensemble`):
|
|
41
41
|
*
|
|
42
42
|
* | Form | Address |
|
|
43
43
|
* | --------- | ------------------------------------------------------------- |
|
|
44
|
-
* | `foundry` | `modules/harn-ensemble/
|
|
45
|
-
* | `local` | `assets/images/map.webp`
|
|
44
|
+
* | `foundry` | `modules/harn-ensemble/images/map.webp` |
|
|
45
|
+
* | `local` | `assets/images/places/map.webp` |
|
|
46
46
|
* | `web` | `https://cdn.heroiclands.org/harnensemble/images/map.webp` |
|
|
47
|
-
* | `pdf` | `assets/images/map.webp`
|
|
47
|
+
* | `pdf` | `assets/images/places/map.webp` |
|
|
48
|
+
*
|
|
49
|
+
* **Foundry and the website serve the deployed layout.** A suffix under
|
|
50
|
+
* `icons/` or `images/` reaches them flat — the type root and the basename,
|
|
51
|
+
* whatever directories the repository nests it in — because that is how the
|
|
52
|
+
* stage lays a package's pictures out and how the CDN holds them; see
|
|
53
|
+
* {@link module:engine/asset-shipping}. Every other suffix keeps its path, and
|
|
54
|
+
* sits under `assets/` in the install. `local` and `pdf` name the source file,
|
|
55
|
+
* so they keep the repository's own arrangement.
|
|
48
56
|
*
|
|
49
57
|
* **`<package>` and `<foundry-id>` are two different names.** The package is
|
|
50
58
|
* `harnensemble` — what the content is called, what the website serves it under,
|
|
@@ -91,6 +99,7 @@
|
|
|
91
99
|
*/
|
|
92
100
|
|
|
93
101
|
import { PACKAGEBUILD_PACKAGE } from "./packages.mjs";
|
|
102
|
+
import { deployedSuffix, isDeployedSuffix } from "./asset-types.mjs";
|
|
94
103
|
|
|
95
104
|
/**
|
|
96
105
|
* The directory a package ships its files in, and the segment that marks a
|
|
@@ -146,7 +155,8 @@ export const PATHNAME_SURFACES = Object.freeze(["foundry", "local", "web", "pdf"
|
|
|
146
155
|
* package owns, `package` for an owned file.
|
|
147
156
|
* @property {string|null} package The content package that owns the file, or
|
|
148
157
|
* `null` when no package does.
|
|
149
|
-
* @property {string|null} suffix The path inside that package's
|
|
158
|
+
* @property {string|null} suffix The path inside that package's `assets/`
|
|
159
|
+
* directory, as its repository arranges it.
|
|
150
160
|
* @property {boolean} own Whether the owner is the package being built, and so
|
|
151
161
|
* whether `local` names a file this build may open.
|
|
152
162
|
* @property {string|null} foundry The address inside a Foundry install.
|
|
@@ -407,6 +417,10 @@ export function resolvePathname(raw, config) {
|
|
|
407
417
|
const suffix = named ? segments.slice(2).join("/") : authored;
|
|
408
418
|
const entry = owner ? packages.get(owner) : undefined;
|
|
409
419
|
const host = String(config.site?.assets ?? "").replace(/\/+$/, "");
|
|
420
|
+
// A picture deploys flat at the package root; anything else keeps its
|
|
421
|
+
// path under the package's `assets/`.
|
|
422
|
+
const deployed = deployedSuffix(suffix);
|
|
423
|
+
const installed = isDeployedSuffix(suffix) ? deployed : `${ASSETS_SEGMENT}/${suffix}`;
|
|
410
424
|
|
|
411
425
|
return {
|
|
412
426
|
authored,
|
|
@@ -418,14 +432,11 @@ export function resolvePathname(raw, config) {
|
|
|
418
432
|
// the first segment names a package this build has never heard of:
|
|
419
433
|
// both are "there is no install path to derive", and guessing one
|
|
420
434
|
// writes an address into a document nobody would check.
|
|
421
|
-
foundry:
|
|
422
|
-
entry?.root && entry.id ?
|
|
423
|
-
`${entry.root}/${entry.id}/${ASSETS_SEGMENT}/${suffix}`
|
|
424
|
-
: null,
|
|
435
|
+
foundry: entry?.root && entry.id ? `${entry.root}/${entry.id}/${installed}` : null,
|
|
425
436
|
local: `${ASSETS_SEGMENT}/${suffix}`,
|
|
426
437
|
// `null` where no asset host is configured. The website is the one
|
|
427
438
|
// surface whose address is not derivable from the repository itself.
|
|
428
|
-
web: host && owner ? `${host}/${owner}/${
|
|
439
|
+
web: host && owner ? `${host}/${owner}/${deployed}` : null,
|
|
429
440
|
pdf: `${ASSETS_SEGMENT}/${suffix}`,
|
|
430
441
|
};
|
|
431
442
|
}
|
package/engine/rank-ladder.mjs
CHANGED
|
@@ -47,18 +47,47 @@ import { RUNG_FIELDS } from "./standing-terms.mjs";
|
|
|
47
47
|
|
|
48
48
|
export { RUNG_FIELDS };
|
|
49
49
|
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Every affiliation's ladder has its two floor rungs: level 0, those the body
|
|
52
|
+
* excludes, and level 1, its ordinary members.
|
|
53
|
+
*
|
|
54
|
+
* A body that can name its members can name those it shuts out, whether or not
|
|
55
|
+
* it ever does, so both rungs are required of every ladder. A missing or empty
|
|
56
|
+
* ladder is one finding naming both; a ladder lacking either rung is one
|
|
57
|
+
* finding per rung it lacks, at the `ranks` key.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} note - The note being checked.
|
|
60
|
+
* @param {string} note.file - Its path, as a diagnostic spells it.
|
|
61
|
+
* @param {string} [note.raw] - Its source, for the position.
|
|
62
|
+
* @param {object} [note.fm] - Its parsed frontmatter.
|
|
63
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
64
|
+
* severity: string, message: string}>} The findings.
|
|
65
|
+
*/
|
|
51
66
|
export function checkAffiliationRankFloor(note) {
|
|
52
67
|
if (note.fm?.type !== "affiliation") return [];
|
|
53
68
|
const governance = note.fm?.data?.governance;
|
|
54
69
|
const ranks = governance?.ranks;
|
|
55
70
|
// A non-list already has a shape finding. Do not repeat it here.
|
|
56
71
|
if (ranks !== undefined && ranks !== null && !Array.isArray(ranks)) return [];
|
|
57
|
-
|
|
72
|
+
const has = (level) =>
|
|
58
73
|
Array.isArray(ranks) &&
|
|
59
|
-
ranks.some((rung) => integer(rung?.level) && Number(rung.level) ===
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
ranks.some((rung) => integer(rung?.level) && Number(rung.level) === level);
|
|
75
|
+
|
|
76
|
+
const messages = [];
|
|
77
|
+
if (!Array.isArray(ranks) || ranks.length === 0) {
|
|
78
|
+
messages.push(
|
|
79
|
+
"affiliation needs data.governance.ranks with a level-0 rung naming those the " +
|
|
80
|
+
"body excludes and a level 1 standing for its members",
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
if (!has(1)) messages.push("affiliation data.governance.ranks needs a level 1 standing");
|
|
84
|
+
if (!has(0))
|
|
85
|
+
messages.push(
|
|
86
|
+
"data.governance.ranks must have a level-0 rung naming those the body " +
|
|
87
|
+
"excludes, beside its level-1 members",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (messages.length === 0) return [];
|
|
62
91
|
|
|
63
92
|
const path = Array.isArray(ranks) ? ["data", "governance", "ranks"] : ["data", "governance"];
|
|
64
93
|
const atRankOrGovernance = positionOfFrontmatterPath(note.raw ?? "", path, { key: true });
|
|
@@ -66,17 +95,12 @@ export function checkAffiliationRankFloor(note) {
|
|
|
66
95
|
atRankOrGovernance.line !== undefined ?
|
|
67
96
|
atRankOrGovernance
|
|
68
97
|
: positionOfFrontmatterPath(note.raw ?? "", ["data"], { key: true });
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
!Array.isArray(ranks) || ranks.length === 0 ?
|
|
76
|
-
"affiliation needs data.governance.ranks with a level 1 standing"
|
|
77
|
-
: "affiliation data.governance.ranks needs a level 1 standing",
|
|
78
|
-
},
|
|
79
|
-
];
|
|
98
|
+
return messages.map((message) => ({
|
|
99
|
+
file: note.file,
|
|
100
|
+
...position,
|
|
101
|
+
severity: "error",
|
|
102
|
+
message,
|
|
103
|
+
}));
|
|
80
104
|
}
|
|
81
105
|
|
|
82
106
|
/**
|
package/engine/scenes.mjs
CHANGED
|
@@ -220,7 +220,7 @@ export class Scenes extends BasePackCompiler {
|
|
|
220
220
|
// A scene that references nothing ships fine as a plain `scenes`
|
|
221
221
|
// entry; only pins need the id-preserving import an Adventure gives.
|
|
222
222
|
if (!place.pinned) continue;
|
|
223
|
-
this.writeTo(this.adventureDir, this.#buildAdventure(place));
|
|
223
|
+
this.writeTo(this.adventureDir, this.#buildAdventure(place), "Adventure");
|
|
224
224
|
this.adventureCount++;
|
|
225
225
|
}
|
|
226
226
|
}
|
|
@@ -82,7 +82,9 @@ export const RUNG_FIELDS = Object.freeze([
|
|
|
82
82
|
kind: "integer",
|
|
83
83
|
required: true,
|
|
84
84
|
shape: "a whole number — the rung's position on this body's own ladder",
|
|
85
|
-
describe:
|
|
85
|
+
describe:
|
|
86
|
+
"The rung's position on the ladder — `0` the excluded, `1` the ordinary member; " +
|
|
87
|
+
"a member's `rank` names it.",
|
|
86
88
|
}),
|
|
87
89
|
Object.freeze({
|
|
88
90
|
name: "title",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.5.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",
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
"@viz-js/viz": "^3.30.0",
|
|
137
137
|
"acorn": "^8.18.0",
|
|
138
138
|
"archiver": "^8.0.0",
|
|
139
|
+
"aws4fetch": "^1.0.20",
|
|
139
140
|
"classic-level": "^3.0.0",
|
|
140
141
|
"dale-chall": "^2.0.1",
|
|
141
142
|
"dotenv": "^18.0.0",
|
|
@@ -189,8 +190,8 @@
|
|
|
189
190
|
"prepack": "run-s build:types build:asset-index",
|
|
190
191
|
"build:types": "tsc -p tsconfig.dts.json",
|
|
191
192
|
"build:asset-index": "node ci/emit-asset-index.mjs",
|
|
192
|
-
"format": "prettier --write
|
|
193
|
-
"format:check": "prettier --check
|
|
193
|
+
"format": "node ci/prettier.mjs --write",
|
|
194
|
+
"format:check": "node ci/prettier.mjs --check",
|
|
194
195
|
"lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:image-roles lint:banners lint:content-format lint:docs-links",
|
|
195
196
|
"lint:markdown": "node bin/package-build.mjs markdown",
|
|
196
197
|
"lint:docs-links": "node bin/package-build.mjs docs links",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-afflictionbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
2
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-armorclothingbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
3
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-attributebnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
4
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-containerbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
5
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-devdocsbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
6
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-miscgearbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
7
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-mysticalabilitybnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
8
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-projectilebnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
9
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-rulesbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
10
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-skillbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
11
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-traumabnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
12
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-userguidebnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
13
|
-
{"address":{"canonical":{"address":"packagebuild-none-image-weapongearbnr","anchor":null,"anchorKind":null}},"asset":{"ai":
|
|
1
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-afflictionbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/afflictionbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"afflictionbnr","type":"image"}
|
|
2
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-armorclothingbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/armorclothingbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"armorclothingbnr","type":"image"}
|
|
3
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-attributebnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/attributebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"attributebnr","type":"image"}
|
|
4
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-containerbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/containerbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"containerbnr","type":"image"}
|
|
5
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-devdocsbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/devdocsbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"devdocsbnr","type":"image"}
|
|
6
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-miscgearbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/miscgearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"miscgearbnr","type":"image"}
|
|
7
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-mysticalabilitybnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/mysticalabilitybnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"mysticalabilitybnr","type":"image"}
|
|
8
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-projectilebnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/projectilebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"projectilebnr","type":"image"}
|
|
9
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-rulesbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/rulesbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"rulesbnr","type":"image"}
|
|
10
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-skillbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/skillbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"skillbnr","type":"image"}
|
|
11
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-traumabnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/traumabnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"traumabnr","type":"image"}
|
|
12
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-userguidebnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/userguidebnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"userguidebnr","type":"image"}
|
|
13
|
+
{"address":{"canonical":{"address":"packagebuild-none-image-weapongearbnr","anchor":null,"anchorKind":null}},"asset":{"ai":true,"attribution":"Tom Rodriguez","height":768,"license":"CC-BY-SA-4.0","notes":"","path":"images/banners/weapongearbnr.webp","role":"","source":"","width":1792},"package":"packagebuild","shortcode":"weapongearbnr","type":"image"}
|
package/release.mjs
CHANGED
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
* by what the manifest says, not chosen here — see `manifest.mjs`. A package
|
|
25
25
|
* that publishes a DataModel schema (`packageBuild.schema`, staged through
|
|
26
26
|
* `packageBuild.assets`) gets a fourth: `schema.json`, published whenever the
|
|
27
|
-
* stage carries one and silently skipped otherwise.
|
|
27
|
+
* stage carries one and silently skipped otherwise. A package that stages
|
|
28
|
+
* pictures gets `icons.zip` and `images.zip` as well: each picture root of the
|
|
29
|
+
* stage with the package's asset set, `asset-sets/default.json`, for a GM
|
|
30
|
+
* placing the files by hand.
|
|
28
31
|
*
|
|
29
32
|
* Kept apart from `stage.mjs` because this is the only part of assembling a
|
|
30
33
|
* package that needs a dependency. A repository that never cuts a release from
|
|
@@ -44,6 +47,7 @@ import path from "node:path";
|
|
|
44
47
|
import { ZipArchive } from "archiver";
|
|
45
48
|
|
|
46
49
|
import { SCHEMA_ARTIFACT_FILE } from "./engine/foreign-catalog.mjs";
|
|
50
|
+
import { DEPLOYED_ASSET_TYPES, SHIPPED_SET_FILE } from "./engine/asset-shipping.mjs";
|
|
47
51
|
|
|
48
52
|
/** The earliest instant a zip entry's date field can hold. */
|
|
49
53
|
const ZIP_EPOCH_MS = Date.UTC(1980, 0, 1);
|
|
@@ -74,9 +78,10 @@ function archiveDate() {
|
|
|
74
78
|
* @param {import("archiver").ZipArchive} archive
|
|
75
79
|
* @param {string} root - The directory whose contents become the archive.
|
|
76
80
|
* @param {Date} date - The date recorded on every entry.
|
|
81
|
+
* @param {string} [prefix] - Prepended to every entry's name, `/`-terminated.
|
|
77
82
|
* @returns {Promise<void>}
|
|
78
83
|
*/
|
|
79
|
-
async function appendTree(archive, root, date) {
|
|
84
|
+
async function appendTree(archive, root, date, prefix = "") {
|
|
80
85
|
/** @param {string} rel - Path relative to `root`, `/`-separated, or "". */
|
|
81
86
|
async function walk(rel) {
|
|
82
87
|
const entries = await fsp.readdir(path.join(root, rel), { withFileTypes: true });
|
|
@@ -88,11 +93,15 @@ async function appendTree(archive, root, date) {
|
|
|
88
93
|
for (const entry of entries) {
|
|
89
94
|
const name = rel ? `${rel}/${entry.name}` : entry.name;
|
|
90
95
|
if (entry.isDirectory()) {
|
|
91
|
-
archive.append(Buffer.alloc(0), {
|
|
96
|
+
archive.append(Buffer.alloc(0), {
|
|
97
|
+
name: `${prefix}${name}/`,
|
|
98
|
+
date,
|
|
99
|
+
mode: 0o755,
|
|
100
|
+
});
|
|
92
101
|
await walk(name);
|
|
93
102
|
} else {
|
|
94
103
|
const body = await fsp.readFile(path.join(root, name));
|
|
95
|
-
archive.append(body, { name
|
|
104
|
+
archive.append(body, { name: `${prefix}${name}`, date, mode: 0o644 });
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
}
|
|
@@ -123,9 +132,10 @@ async function appendTree(archive, root, date) {
|
|
|
123
132
|
* @param {boolean} [opts.pdf] - Whether to build the books that ship beside the
|
|
124
133
|
* archive. `true` by default; `false` skips the build and reports the skip.
|
|
125
134
|
* @returns {Promise<{zip: string, manifest: string, metadata?: string,
|
|
126
|
-
* schema?: string,
|
|
127
|
-
* bytes: number, version: string}>} The paths
|
|
128
|
-
* found, the archive's size, and the version
|
|
135
|
+
* schema?: string, pictures: string[], pdfs: string[], pdfFindings: object[],
|
|
136
|
+
* pdfSkipped: string|null, bytes: number, version: string}>} The paths
|
|
137
|
+
* written, what the book build found, the archive's size, and the version
|
|
138
|
+
* the manifest declares. `pictures` is each picture archive written.
|
|
129
139
|
* `metadata` is absent when the manifest advertises no content index,
|
|
130
140
|
* `schema` is absent when the stage carries no `schema.json`, and `pdfs` is
|
|
131
141
|
* every book's PDF that was written, in declared order. `pdfSkipped` is
|
|
@@ -184,6 +194,7 @@ export async function packRelease({
|
|
|
184
194
|
|
|
185
195
|
const metadata = await publishMetadataIndex({ manifest, stage, out, metadataDir });
|
|
186
196
|
const schema = await publishSchemaAsset({ stage, out });
|
|
197
|
+
const pictures = await publishPictureArchives({ stage, out });
|
|
187
198
|
|
|
188
199
|
// Last, and never fatal: the archive and the manifest are the release, and
|
|
189
200
|
// a book that failed to set is a reported problem rather than a reason to
|
|
@@ -198,6 +209,7 @@ export async function packRelease({
|
|
|
198
209
|
manifest: path.join(out, manifestName),
|
|
199
210
|
...(metadata ? { metadata } : {}),
|
|
200
211
|
...(schema ? { schema } : {}),
|
|
212
|
+
pictures,
|
|
201
213
|
pdfs: book.pdfs,
|
|
202
214
|
pdfFindings: book.findings,
|
|
203
215
|
pdfSkipped: book.pdfs.length ? null : book.reason,
|
|
@@ -324,3 +336,53 @@ async function publishSchemaAsset({ stage, out }) {
|
|
|
324
336
|
await fsp.copyFile(src, dest);
|
|
325
337
|
return dest;
|
|
326
338
|
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Zip each picture root of the stage, with its asset set, beside the
|
|
342
|
+
* archive — `icons.zip` and `images.zip`.
|
|
343
|
+
*
|
|
344
|
+
* Each archive unpacks at a location's root: `icons/` or `images/`, and the
|
|
345
|
+
* `asset-sets/default.json` listing every picture the package ships. A stage
|
|
346
|
+
* with no such set staged no pictures and publishes neither.
|
|
347
|
+
*
|
|
348
|
+
* @param {object} opts
|
|
349
|
+
* @param {string} opts.stage - The staged tree.
|
|
350
|
+
* @param {string} opts.out - Where release assets are written.
|
|
351
|
+
* @returns {Promise<string[]>} The archives written.
|
|
352
|
+
*/
|
|
353
|
+
async function publishPictureArchives({ stage, out }) {
|
|
354
|
+
const list = path.join(stage, ...SHIPPED_SET_FILE.split("/"));
|
|
355
|
+
if (!fs.existsSync(list)) return [];
|
|
356
|
+
const written = [];
|
|
357
|
+
for (const { root } of DEPLOYED_ASSET_TYPES) {
|
|
358
|
+
const dir = path.join(stage, root);
|
|
359
|
+
if (!fs.existsSync(dir)) continue;
|
|
360
|
+
const zipPath = path.join(out, `${root}.zip`);
|
|
361
|
+
const output = fs.createWriteStream(zipPath);
|
|
362
|
+
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
363
|
+
const closed = new Promise((resolve, reject) => {
|
|
364
|
+
output.on("close", resolve);
|
|
365
|
+
output.on("error", reject);
|
|
366
|
+
archive.on("error", reject);
|
|
367
|
+
archive.on("warning", reject);
|
|
368
|
+
});
|
|
369
|
+
archive.pipe(output);
|
|
370
|
+
const date = archiveDate();
|
|
371
|
+
archive.append(Buffer.alloc(0), {
|
|
372
|
+
name: `${path.posix.dirname(SHIPPED_SET_FILE)}/`,
|
|
373
|
+
date,
|
|
374
|
+
mode: 0o755,
|
|
375
|
+
});
|
|
376
|
+
archive.append(await fsp.readFile(list), {
|
|
377
|
+
name: SHIPPED_SET_FILE,
|
|
378
|
+
date,
|
|
379
|
+
mode: 0o644,
|
|
380
|
+
});
|
|
381
|
+
archive.append(Buffer.alloc(0), { name: `${root}/`, date, mode: 0o755 });
|
|
382
|
+
await appendTree(archive, dir, date, `${root}/`);
|
|
383
|
+
await archive.finalize();
|
|
384
|
+
await closed;
|
|
385
|
+
written.push(zipPath);
|
|
386
|
+
}
|
|
387
|
+
return written;
|
|
388
|
+
}
|
package/sohl/actors.mjs
CHANGED
|
@@ -55,7 +55,13 @@
|
|
|
55
55
|
* @module
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
sohlField,
|
|
60
|
+
resolveName,
|
|
61
|
+
systemTemplatePriority,
|
|
62
|
+
folderField,
|
|
63
|
+
resolveImg,
|
|
64
|
+
} from "../engine/helpers.mjs";
|
|
59
65
|
import { openingMasteryLevel } from "./skill-base.mjs";
|
|
60
66
|
import { SystemActorCompiler, renderSection } from "../engine/actor-compiler.mjs";
|
|
61
67
|
import { withDraftNotice } from "../engine/draft-notice.mjs";
|
|
@@ -84,9 +90,11 @@ const SYSTEM = SOHL_DOCUMENT_SUBTYPES.block;
|
|
|
84
90
|
|
|
85
91
|
// Default art per actor **subtype**, applied when frontmatter supplies no `img`
|
|
86
92
|
// / `portrait`. Beings default to the generic person icon; another subtype adds
|
|
87
|
-
// its own entry here as the map gains a row for it.
|
|
93
|
+
// its own entry here as the map gains a row for it. Each is an authored
|
|
94
|
+
// pathname, resolved like any `img:`, so its Foundry path follows the deployed
|
|
95
|
+
// layout of the package that ships it.
|
|
88
96
|
const DEFAULT_IMG = {
|
|
89
|
-
being: "
|
|
97
|
+
being: "sohl/assets/icons/game-icons/delapouite/person.svg",
|
|
90
98
|
};
|
|
91
99
|
|
|
92
100
|
/**
|
|
@@ -313,7 +321,7 @@ export class Actors extends SystemActorCompiler {
|
|
|
313
321
|
// The being's own default sits above the subtype's: only the note's
|
|
314
322
|
// tags say whether it is a person or a creature, and only this pass
|
|
315
323
|
// reads them.
|
|
316
|
-
const art = this.actorArt(fm, defaultActorImg(subType));
|
|
324
|
+
const art = this.actorArt(fm, resolveImg(defaultActorImg(subType)));
|
|
317
325
|
|
|
318
326
|
const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
|
|
319
327
|
|
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
* Injecting the map through configuration instead would re-open that drift,
|
|
39
39
|
* because the two sides would once more be free to disagree.
|
|
40
40
|
*
|
|
41
|
-
* **
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* **Values are the deployed form of the file's path**, `sohl/icons/<basename>`:
|
|
42
|
+
* the owning package's name, then the flat location a staged package serves its
|
|
43
|
+
* icons from. A runtime prefixes `systems/` and has a Foundry address.
|
|
44
|
+
* {@link defaultItemArt} returns the same file as an authored pathname,
|
|
45
|
+
* `sohl/assets/icons/<basename>`, which is what the build resolves through
|
|
46
|
+
* `resolveImg`. If a new item type is added, add its default here.
|
|
45
47
|
*
|
|
46
48
|
* **The keys are SoHL *document* subtypes, not markdown note types.** The
|
|
47
49
|
* runtime reads this with `itemData.type`, which is a Foundry Item subtype, and
|
|
@@ -53,19 +55,19 @@
|
|
|
53
55
|
* build translates once and no second copy of those three rows exists.
|
|
54
56
|
*/
|
|
55
57
|
export const DEFAULT_ITEM_ART = {
|
|
56
|
-
affiliation: "sohl/
|
|
57
|
-
affliction: "sohl/
|
|
58
|
-
armorgear: "sohl/
|
|
59
|
-
attribute: "sohl/
|
|
60
|
-
concoctiongear: "sohl/
|
|
61
|
-
containergear: "sohl/
|
|
62
|
-
miscgear: "sohl/
|
|
63
|
-
mystery: "sohl/
|
|
64
|
-
mysticalability: "sohl/
|
|
65
|
-
projectilegear: "sohl/
|
|
66
|
-
skill: "sohl/
|
|
67
|
-
trauma: "sohl/
|
|
68
|
-
weapongear: "sohl/
|
|
58
|
+
affiliation: "sohl/icons/shieldnn.svg",
|
|
59
|
+
affliction: "sohl/icons/sick.svg",
|
|
60
|
+
armorgear: "sohl/icons/breastplate.svg",
|
|
61
|
+
attribute: "sohl/icons/charm.svg",
|
|
62
|
+
concoctiongear: "sohl/icons/flask.svg",
|
|
63
|
+
containergear: "sohl/icons/sack.svg",
|
|
64
|
+
miscgear: "sohl/icons/questionmark.svg",
|
|
65
|
+
mystery: "sohl/icons/sparkles.svg",
|
|
66
|
+
mysticalability: "sohl/icons/handsparkles.svg",
|
|
67
|
+
projectilegear: "sohl/icons/arrow.svg",
|
|
68
|
+
skill: "sohl/icons/headgear.svg",
|
|
69
|
+
trauma: "sohl/icons/injury.svg",
|
|
70
|
+
weapongear: "sohl/icons/sword.svg",
|
|
69
71
|
};
|
|
70
72
|
|
|
71
73
|
/**
|
|
@@ -80,7 +82,8 @@ export const DEFAULT_ITEM_ART = {
|
|
|
80
82
|
*
|
|
81
83
|
* @param {string} type - The SoHL Item document subtype — `armorgear`, not the
|
|
82
84
|
* `armor` note type that compiles into one.
|
|
83
|
-
* @returns {string} the default image
|
|
85
|
+
* @returns {string} the default image as an authored pathname,
|
|
86
|
+
* `sohl/assets/icons/<basename>`.
|
|
84
87
|
*/
|
|
85
88
|
export function defaultItemArt(type) {
|
|
86
89
|
if (!(type in DEFAULT_ITEM_ART)) {
|
|
@@ -91,5 +94,6 @@ export function defaultItemArt(type) {
|
|
|
91
94
|
// The map's inferred type has no index signature — deliberately, so callers
|
|
92
95
|
// reading it directly get the precise key set. The `in` guard above has
|
|
93
96
|
// already proved this key is present.
|
|
94
|
-
|
|
97
|
+
const deployed = /** @type {Record<string, string>} */ (DEFAULT_ITEM_ART)[type];
|
|
98
|
+
return deployed.replace("/icons/", "/assets/icons/");
|
|
95
99
|
}
|
package/types/config.d.mts
CHANGED
|
@@ -102,6 +102,8 @@ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
|
|
|
102
102
|
* @property {string} stageDir The staged package root, relative to
|
|
103
103
|
* `rootDir`. Every asset `to:` lands under it.
|
|
104
104
|
* @property {readonly Readonly<AssetSpec>[]} assets
|
|
105
|
+
* @property {readonly string[]} shippingLicenses Licences this package adds to the
|
|
106
|
+
* default shipping list.
|
|
105
107
|
* @property {boolean} baseStyles Whether the package takes the shared base
|
|
106
108
|
* stylesheet, staged into `stageDir` and
|
|
107
109
|
* named first in the manifest's `styles`.
|
|
@@ -238,6 +240,11 @@ export type PackageBuildConfig = {
|
|
|
238
240
|
*/
|
|
239
241
|
stageDir: string;
|
|
240
242
|
assets: readonly Readonly<AssetSpec>[];
|
|
243
|
+
/**
|
|
244
|
+
* Licences this package adds to the
|
|
245
|
+
* default shipping list.
|
|
246
|
+
*/
|
|
247
|
+
shippingLicenses: readonly string[];
|
|
241
248
|
/**
|
|
242
249
|
* Whether the package takes the shared base
|
|
243
250
|
* stylesheet, staged into `stageDir` and
|