@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +29 -25
  3. package/assets/images/provenance.yaml +1 -0
  4. package/bin/package-build.mjs +259 -28
  5. package/ci/prettier.mjs +73 -0
  6. package/config.mjs +10 -0
  7. package/container.mjs +232 -1
  8. package/content-config.mjs +5 -1
  9. package/docs/api.md +100 -52
  10. package/docs/authoring/assets.md +16 -1
  11. package/docs/commands.md +154 -7
  12. package/docs/configuration.md +39 -2
  13. package/docs/content-format.md +30 -6
  14. package/docs/guides/e2e.md +38 -0
  15. package/docs/guides/icons.md +5 -0
  16. package/docs/guides/release-and-deploy.md +36 -1
  17. package/docs/reference/format-details.md +180 -34
  18. package/docs/reference/note-types.md +69 -69
  19. package/engine/asset-index.mjs +63 -22
  20. package/engine/asset-references.mjs +355 -0
  21. package/engine/asset-shipping.mjs +363 -0
  22. package/engine/asset-types.mjs +70 -0
  23. package/engine/base-compiler.mjs +24 -1
  24. package/engine/cdn-publish.mjs +618 -0
  25. package/engine/index.mjs +6 -0
  26. package/engine/note-vocabulary.mjs +4 -1
  27. package/engine/pathnames.mjs +21 -10
  28. package/engine/rank-ladder.mjs +40 -16
  29. package/engine/scenes.mjs +1 -1
  30. package/engine/standing-terms.mjs +3 -1
  31. package/package.json +4 -3
  32. package/packagebuild-metadata.jsonl +13 -13
  33. package/release.mjs +69 -7
  34. package/sohl/actors.mjs +12 -4
  35. package/sohl/default-item-art.mjs +23 -19
  36. package/types/config.d.mts +7 -0
  37. package/types/container.d.mts +137 -1
  38. package/types/engine/asset-index.d.mts +3 -2
  39. package/types/engine/asset-references.d.mts +136 -0
  40. package/types/engine/asset-shipping.d.mts +240 -0
  41. package/types/engine/asset-types.d.mts +36 -0
  42. package/types/engine/base-compiler.d.mts +7 -1
  43. package/types/engine/cdn-publish.d.mts +311 -0
  44. package/types/engine/index.d.mts +2 -0
  45. package/types/engine/pathnames.d.mts +2 -1
  46. package/types/engine/rank-ladder.d.mts +25 -6
  47. package/types/release.d.mts +5 -3
  48. package/types/sohl/default-item-art.d.mts +2 -1
@@ -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/assets/images/map.webp` |
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 shipped tree.
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}/${suffix}` : null,
439
+ web: host && owner ? `${host}/${owner}/${deployed}` : null,
429
440
  pdf: `${ASSETS_SEGMENT}/${suffix}`,
430
441
  };
431
442
  }
@@ -47,18 +47,47 @@ import { RUNG_FIELDS } from "./standing-terms.mjs";
47
47
 
48
48
  export { RUNG_FIELDS };
49
49
 
50
- /** Every affiliation has an ordinary standing that a being can name as rank 1. */
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
- if (
72
+ const has = (level) =>
58
73
  Array.isArray(ranks) &&
59
- ranks.some((rung) => integer(rung?.level) && Number(rung.level) === 1)
60
- )
61
- return [];
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
- file: note.file,
72
- ...position,
73
- severity: "error",
74
- message:
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: "The rung's position on the ladder; a member's `rank` names it.",
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.4.1",
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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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":false,"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"}
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), { name: `${name}/`, date, mode: 0o755 });
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, date, mode: 0o644 });
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, pdfs: string[], pdfFindings: object[], pdfSkipped: string|null,
127
- * bytes: number, version: string}>} The paths written, what the book build
128
- * found, the archive's size, and the version the manifest declares.
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 { sohlField, resolveName, systemTemplatePriority, folderField } from "../engine/helpers.mjs";
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: "systems/sohl/assets/icons/game-icons/delapouite/person.svg",
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
- * **Paths name the package that owns the file**, as every pathname does: these
42
- * icons are the `sohl` package's, so they are written `sohl/assets/…` and each
43
- * surface derives its own address from that. If a new item type is added, add
44
- * its default here.
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/assets/icons/noun/shieldnn.svg",
57
- affliction: "sohl/assets/icons/other/sick.svg",
58
- armorgear: "sohl/assets/icons/game-icons/lorc/breastplate.svg",
59
- attribute: "sohl/assets/icons/game-icons/lorc/charm.svg",
60
- concoctiongear: "sohl/assets/icons/game-icons/badges/flask.svg",
61
- containergear: "sohl/assets/icons/other/sack.svg",
62
- miscgear: "sohl/assets/icons/other/questionmark.svg",
63
- mystery: "sohl/assets/icons/game-icons/delapouite/sparkles.svg",
64
- mysticalability: "sohl/assets/icons/other/handsparkles.svg",
65
- projectilegear: "sohl/assets/icons/noun/arrow.svg",
66
- skill: "sohl/assets/icons/other/headgear.svg",
67
- trauma: "sohl/assets/icons/other/injury.svg",
68
- weapongear: "sohl/assets/icons/other/sword.svg",
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 path for that subtype.
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
- return /** @type {Record<string, string>} */ (DEFAULT_ITEM_ART)[type];
97
+ const deployed = /** @type {Record<string, string>} */ (DEFAULT_ITEM_ART)[type];
98
+ return deployed.replace("/icons/", "/assets/icons/");
95
99
  }
@@ -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