@heroiclands/package-build 21.1.0 → 22.0.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 +245 -0
- package/CONTENT.md +107 -3
- package/assets/fonts/libertinus/LibertinusMono-Regular.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Bold.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Italic.otf +0 -0
- package/assets/fonts/libertinus/LibertinusSans-Regular.otf +0 -0
- package/assets/fonts/libertinus/OFL.txt +94 -0
- package/assets/fonts/libertinus/provenance.yaml +11 -0
- package/assets/images/banners/afflictionbnr.webp +0 -0
- package/assets/images/banners/armorclothingbnr.webp +0 -0
- package/assets/images/banners/attributebnr.webp +0 -0
- package/assets/images/banners/containerbnr.webp +0 -0
- package/assets/images/banners/devdocsbnr.webp +0 -0
- package/assets/images/banners/miscgearbnr.webp +0 -0
- package/assets/images/banners/mysticalabilitybnr.webp +0 -0
- package/assets/images/banners/projectilebnr.webp +0 -0
- package/assets/images/banners/rulesbnr.webp +0 -0
- package/assets/images/banners/skillbnr.webp +0 -0
- package/assets/images/banners/traumabnr.webp +0 -0
- package/assets/images/banners/userguidebnr.webp +0 -0
- package/assets/images/banners/weapongearbnr.webp +0 -0
- package/assets/images/provenance.yaml +4 -0
- package/bin/content-build.mjs +19 -5
- package/bin/package-build.mjs +6 -5
- package/ci/emit-asset-index.mjs +32 -0
- package/content-config.mjs +30 -1
- package/docs/api.md +103 -27
- package/docs/commands.md +13 -11
- package/docs/configuration.md +16 -7
- package/docs/content-format.md +523 -111
- package/engine/actor-compiler.mjs +58 -9
- package/engine/art-fields.mjs +292 -0
- package/engine/asset-index.mjs +397 -0
- package/engine/asset-types.mjs +192 -0
- package/engine/base-compiler.mjs +69 -1
- package/engine/bundles.mjs +5 -5
- package/engine/compile-corpus.mjs +4 -0
- package/engine/content-address.mjs +14 -2
- package/engine/content-charset.mjs +5 -1
- package/engine/content-embeds.mjs +314 -0
- package/engine/content-html.mjs +5 -1
- package/engine/content-icons.mjs +5 -1
- package/engine/content-images.mjs +26 -5
- package/engine/content-index.mjs +68 -33
- package/engine/content-links.mjs +164 -24
- package/engine/dependency-bump.mjs +14 -16
- package/engine/frontmatter-lint.mjs +36 -23
- package/engine/helpers.mjs +83 -31
- package/engine/index-records.mjs +63 -4
- package/engine/index.mjs +15 -0
- package/engine/infobox-registry.mjs +50 -3
- package/engine/infobox.mjs +12 -8
- package/engine/item-compiler.mjs +10 -10
- package/engine/journals.mjs +3 -0
- package/engine/macros.mjs +16 -13
- package/engine/map-notes.mjs +29 -22
- package/engine/metadata-index.mjs +44 -36
- package/engine/note-vocabulary.mjs +71 -29
- package/engine/pack-router.mjs +14 -0
- package/engine/packagebuild-index.mjs +124 -0
- package/engine/packages.mjs +83 -0
- package/engine/pathnames.mjs +74 -0
- package/engine/pdf-build.mjs +133 -8
- package/engine/pdf-render.mjs +49 -12
- package/engine/scenes.mjs +12 -9
- package/engine/site-build.mjs +43 -16
- package/engine/site-index.mjs +10 -1
- package/engine/subtype-registry.mjs +21 -0
- package/engine/web-wikilinks.mjs +46 -12
- package/engine/wikilink-syntax.mjs +20 -2
- package/engine/wikilinks.mjs +15 -2
- package/hm3/actors.mjs +11 -38
- package/manifest.mjs +67 -13
- package/package.json +5 -2
- package/packagebuild-metadata.jsonl +13 -0
- package/sohl/actors.mjs +12 -86
- package/sohl/note-schemas.mjs +4 -16
- package/types/content-config.d.mts +8 -0
- package/types/engine/actor-compiler.d.mts +29 -0
- package/types/engine/art-fields.d.mts +192 -0
- package/types/engine/asset-index.d.mts +92 -0
- package/types/engine/asset-types.d.mts +110 -0
- package/types/engine/base-compiler.d.mts +42 -1
- package/types/engine/compile-corpus.d.mts +4 -1
- package/types/engine/content-charset.d.mts +4 -0
- package/types/engine/content-embeds.d.mts +218 -0
- package/types/engine/content-html.d.mts +4 -0
- package/types/engine/content-icons.d.mts +4 -0
- package/types/engine/content-images.d.mts +15 -2
- package/types/engine/content-index.d.mts +27 -15
- package/types/engine/dependency-bump.d.mts +2 -2
- package/types/engine/frontmatter-lint.d.mts +11 -14
- package/types/engine/helpers.d.mts +7 -2
- package/types/engine/index-records.d.mts +44 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +36 -1
- package/types/engine/infobox.d.mts +6 -4
- package/types/engine/macros.d.mts +9 -5
- package/types/engine/map-notes.d.mts +10 -6
- package/types/engine/metadata-index.d.mts +2 -17
- package/types/engine/note-vocabulary.d.mts +30 -0
- package/types/engine/packagebuild-index.d.mts +56 -0
- package/types/engine/packages.d.mts +62 -0
- package/types/engine/pathnames.d.mts +41 -81
- package/types/engine/pdf-build.d.mts +58 -1
- package/types/engine/pdf-render.d.mts +3 -1
- package/types/engine/scenes.d.mts +2 -1
- package/types/engine/site-build.d.mts +5 -1
- package/types/engine/site-index.d.mts +4 -1
- package/types/engine/subtype-registry.d.mts +20 -0
- package/types/engine/web-wikilinks.d.mts +14 -4
- package/types/engine/wikilink-syntax.d.mts +13 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/manifest.d.mts +42 -1
- package/types/sohl/actors.d.mts +0 -1
package/engine/map-notes.mjs
CHANGED
|
@@ -59,7 +59,6 @@ import { CURATED_REGION_EVENTS, EXCLUDED_REGION_EVENTS } from "./region-events.m
|
|
|
59
59
|
// A map's background art is `img`, as every other note type's art is. `image`,
|
|
60
60
|
// the spelling a map alone once used, is retired and gone.
|
|
61
61
|
import { sohlField } from "./frontmatter.mjs";
|
|
62
|
-
import { resolveImg } from "./helpers.mjs";
|
|
63
62
|
|
|
64
63
|
/* -------------------------------------------------------------------- */
|
|
65
64
|
/* Note types and their canvas profiles */
|
|
@@ -880,7 +879,8 @@ function buildRegion(key, spec, geom, ctx) {
|
|
|
880
879
|
* `packageId`, `journalEntryId`, `journalPack` (the pack the note's derived
|
|
881
880
|
* JournalEntry landed in), `pageIds` (heading key → page id),
|
|
882
881
|
* `resolveRegionRef` / `resolveBehaviorRef` / `resolveEffectRef` (address →
|
|
883
|
-
* UUID), `
|
|
882
|
+
* UUID), `art` (an art address → the path each surface serves),
|
|
883
|
+
* `knownActions`, `warnings`, and optionally `folder` and `stats`.
|
|
884
884
|
* @returns {object} The Scene document, keyed for the pack.
|
|
885
885
|
* @throws {Error} On any authoring mistake Foundry would accept silently.
|
|
886
886
|
*/
|
|
@@ -905,11 +905,11 @@ export function buildScene(fm, ctx) {
|
|
|
905
905
|
"match the art",
|
|
906
906
|
);
|
|
907
907
|
}
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
const img =
|
|
912
|
-
if (!img) throw new Error("a map note needs
|
|
908
|
+
// A Scene has no `img`, so a map's background is its own slot, authored
|
|
909
|
+
// under `data:` with the other art. It is an `image` address like every
|
|
910
|
+
// other art reference, and the record it resolves to carries the path.
|
|
911
|
+
const img = ctx.art(fm.data?.bgImage, "bgImage", "image");
|
|
912
|
+
if (!img) throw new Error("a map note needs a `bgImage`");
|
|
913
913
|
|
|
914
914
|
const warn = (message) => {
|
|
915
915
|
if (ctx.warnings) ctx.warnings.push(message);
|
|
@@ -934,7 +934,7 @@ export function buildScene(fm, ctx) {
|
|
|
934
934
|
tokenVision: profile.tokenVision,
|
|
935
935
|
fog: { mode: profile.fog.mode },
|
|
936
936
|
initialLevel: DEFAULT_LEVEL_ID,
|
|
937
|
-
levels: [buildLevel(sohl, sceneId, img)],
|
|
937
|
+
levels: [buildLevel(sohl, sceneId, img, ctx.art)],
|
|
938
938
|
drawings: [],
|
|
939
939
|
tokens: [],
|
|
940
940
|
lights: buildLights(sohl, geom, inner),
|
|
@@ -980,13 +980,16 @@ export function buildScene(fm, ctx) {
|
|
|
980
980
|
*
|
|
981
981
|
* @param {object} sohl - The note's `sohl:` block.
|
|
982
982
|
* @param {string} sceneId - The owning scene's `_id`.
|
|
983
|
-
* @param {string}
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
983
|
+
* @param {string} img - The background art, already resolved from the note's
|
|
984
|
+
* `bgImage` address by {@link buildScene}. Passed rather than resolved here,
|
|
985
|
+
* because an address is answered by the compile's index and this function
|
|
986
|
+
* takes none.
|
|
987
|
+
* @param {(value: unknown, key: string, type: string) => string|null} [art] -
|
|
988
|
+
* The art resolver, for the foreground overlay. Omitted, a note naming one
|
|
989
|
+
* gets no overlay rather than a path nothing serves.
|
|
987
990
|
* @returns {object} The Level document, keyed for the pack.
|
|
988
991
|
*/
|
|
989
|
-
export function buildLevel(sohl, sceneId, img =
|
|
992
|
+
export function buildLevel(sohl, sceneId, img, art = () => null) {
|
|
990
993
|
const level = {
|
|
991
994
|
_id: DEFAULT_LEVEL_ID,
|
|
992
995
|
name: sohl.levelName ?? "Ground",
|
|
@@ -995,9 +998,9 @@ export function buildLevel(sohl, sceneId, img = resolveImg(sohlField({ sohl }, "
|
|
|
995
998
|
color: sohl.backgroundColor ?? "#999999",
|
|
996
999
|
src: img,
|
|
997
1000
|
},
|
|
998
|
-
// The overlay is
|
|
999
|
-
// same rule — a scene draws the two from one
|
|
1000
|
-
foreground: { src:
|
|
1001
|
+
// The overlay is an `image` address like the background, and resolves
|
|
1002
|
+
// by the same rule — a scene draws the two from one statement each.
|
|
1003
|
+
foreground: { src: art(sohl.overlay ?? null, "overlay", "image") },
|
|
1001
1004
|
sort: 0,
|
|
1002
1005
|
_key: `!scenes.levels!${sceneId}.${DEFAULT_LEVEL_ID}`,
|
|
1003
1006
|
};
|
|
@@ -1120,9 +1123,11 @@ export function buildTiles(sohl, geom, ctx) {
|
|
|
1120
1123
|
rotation: spec.rotation ?? 0,
|
|
1121
1124
|
alpha: spec.alpha ?? 1,
|
|
1122
1125
|
sort: 0,
|
|
1123
|
-
// A tile's texture is
|
|
1124
|
-
// scene's own background is.
|
|
1125
|
-
|
|
1126
|
+
// A tile's texture is an address, resolved by the same rule the
|
|
1127
|
+
// scene's own background is. Its key is named for the type it
|
|
1128
|
+
// reaches, so a tile placing a glyph rather than artwork qualifies
|
|
1129
|
+
// — `sohl-none-icon-chest` — and the default needs no memorising.
|
|
1130
|
+
texture: { src: ctx.art(spec.image, "image", "image") },
|
|
1126
1131
|
_key: `!scenes.tiles!${ctx.sceneId}.${id}`,
|
|
1127
1132
|
};
|
|
1128
1133
|
});
|
|
@@ -1143,7 +1148,7 @@ export function buildSounds(sohl, geom, ctx) {
|
|
|
1143
1148
|
return Object.entries(sohl.sounds ?? {}).map(([key, spec]) => {
|
|
1144
1149
|
const label = `sounds.${key}`;
|
|
1145
1150
|
const [x, y] = requirePosition(spec.position, { ...geom, label });
|
|
1146
|
-
if (!spec.
|
|
1151
|
+
if (!spec.audio) throw new Error(`${label}: an ambient sound needs an \`audio\` address`);
|
|
1147
1152
|
const id = spec._id || makeId("scene-sound", `${ctx.sceneId}:${key}`);
|
|
1148
1153
|
return {
|
|
1149
1154
|
_id: id,
|
|
@@ -1151,8 +1156,10 @@ export function buildSounds(sohl, geom, ctx) {
|
|
|
1151
1156
|
x,
|
|
1152
1157
|
y,
|
|
1153
1158
|
radius: spec.radius ?? 0,
|
|
1154
|
-
// An ambient sound is a file a package ships, like the pictures
|
|
1155
|
-
|
|
1159
|
+
// An ambient sound is a file a package ships, like the pictures, so
|
|
1160
|
+
// it is an address too. The emitted field stays Foundry's own
|
|
1161
|
+
// `path`, which is what a Scene's AmbientSound calls it.
|
|
1162
|
+
path: ctx.art(spec.audio, "audio", "audio"),
|
|
1156
1163
|
repeat: spec.repeat ?? true,
|
|
1157
1164
|
volume: spec.volume ?? 0.5,
|
|
1158
1165
|
walls: spec.walls ?? true,
|
|
@@ -43,6 +43,12 @@ import fs from "node:fs";
|
|
|
43
43
|
import path from "node:path";
|
|
44
44
|
|
|
45
45
|
import { formatDiagnostic, positionOfLiteral } from "./diagnostics.mjs";
|
|
46
|
+
// The artifact's name, declared with the package registry so the toolchain's
|
|
47
|
+
// own index can name its file without importing this module back.
|
|
48
|
+
import { metadataFileName, PACKAGEBUILD_PACKAGE } from "./packages.mjs";
|
|
49
|
+
import { packageBuildRecords } from "./packagebuild-index.mjs";
|
|
50
|
+
|
|
51
|
+
export { metadataFileName };
|
|
46
52
|
import { PACKAGE_BASE, readCanonicalKey, resolvePackageUrl } from "./content-address.mjs";
|
|
47
53
|
|
|
48
54
|
/**
|
|
@@ -67,26 +73,6 @@ const STAMP = ".complete";
|
|
|
67
73
|
*/
|
|
68
74
|
export const METADATA_RELATIONSHIP_KINDS = Object.freeze(["systems", "requires"]);
|
|
69
75
|
|
|
70
|
-
/**
|
|
71
|
-
* What a package's content index is called, wherever it is written or fetched.
|
|
72
|
-
*
|
|
73
|
-
* **The local index and the published artifact are one file.** A package emits
|
|
74
|
-
* this, ships it as a release asset, and advertises it as `flags.metadataUrl`;
|
|
75
|
-
* a consumer fetches that same file into its cache and reads it. Naming it in
|
|
76
|
-
* one function is what keeps the emitter, the release and the fetcher from
|
|
77
|
-
* drifting into three spellings of one artifact.
|
|
78
|
-
*
|
|
79
|
-
* The `-metadata` suffix earns its place: a bare `<package>.jsonl` says nothing
|
|
80
|
-
* about what it holds, and these files land in a cache directory beside other
|
|
81
|
-
* packages' artifacts where the name is all a reader has.
|
|
82
|
-
*
|
|
83
|
-
* @param {string} pkg - The content package name.
|
|
84
|
-
* @returns {string} The file name, e.g. `sohl-metadata.jsonl`.
|
|
85
|
-
*/
|
|
86
|
-
export function metadataFileName(pkg) {
|
|
87
|
-
return `${pkg}-metadata.jsonl`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
76
|
/**
|
|
91
77
|
* Every dependency whose published index this build resolves addresses through.
|
|
92
78
|
*
|
|
@@ -312,22 +298,14 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
312
298
|
const packages = new Set();
|
|
313
299
|
const stale = [];
|
|
314
300
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
} catch (err) {
|
|
324
|
-
stale.push({ package: packageOfCache(file), reason: `unreadable: ${err.message}` });
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const pkg = records[0]?.package ?? packageOfCache(file);
|
|
329
|
-
if (local.has(pkg)) continue;
|
|
330
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Fold one package's records into the index.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} pkg - The package that published them.
|
|
305
|
+
* @param {Array<Record<string, any>>} records - Its index records.
|
|
306
|
+
* @returns {void}
|
|
307
|
+
*/
|
|
308
|
+
const ingest = (pkg, records) => {
|
|
331
309
|
// A base is only needed to resolve a page *URL*, so a pack-only
|
|
332
310
|
// dependency — Foundry addresses and no site, which `kethira` is by
|
|
333
311
|
// licensing rather than by accident — needs none. Demanding one would
|
|
@@ -367,10 +345,40 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
367
345
|
// an infobox sorting a being's skills — has no other way to
|
|
368
346
|
// learn it about a note another package publishes.
|
|
369
347
|
subType: record.subType ?? undefined,
|
|
348
|
+
// A file the other package ships, carried through whole. Its
|
|
349
|
+
// `path` is that package's path, so a consumer joins its own
|
|
350
|
+
// root onto it and resolves in one step — which is the point of
|
|
351
|
+
// the record naming a path at all.
|
|
352
|
+
asset: record.asset ?? undefined,
|
|
370
353
|
package: pkg,
|
|
371
354
|
});
|
|
372
355
|
}
|
|
373
356
|
packages.add(pkg);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// The toolchain's own files, first and unconditionally. `packagebuild` is
|
|
360
|
+
// an npm dependency of every consumer rather than a Foundry package, so
|
|
361
|
+
// there is nothing to declare and nothing to fetch — the tree is already on
|
|
362
|
+
// disk beside this module. The special case is entirely in the
|
|
363
|
+
// *acquisition*: the records join the index like any other package's, so
|
|
364
|
+
// every lookup stays one path.
|
|
365
|
+
ingest(PACKAGEBUILD_PACKAGE, packageBuildRecords());
|
|
366
|
+
|
|
367
|
+
for (const file of cachedMetadataFiles(config)) {
|
|
368
|
+
let records;
|
|
369
|
+
try {
|
|
370
|
+
records = fs
|
|
371
|
+
.readFileSync(file, "utf8")
|
|
372
|
+
.split("\n")
|
|
373
|
+
.filter((line) => line.trim())
|
|
374
|
+
.map((line) => JSON.parse(line));
|
|
375
|
+
} catch (err) {
|
|
376
|
+
stale.push({ package: packageOfCache(file), reason: `unreadable: ${err.message}` });
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const pkg = records[0]?.package ?? packageOfCache(file);
|
|
380
|
+
if (local.has(pkg)) continue;
|
|
381
|
+
ingest(pkg, records);
|
|
374
382
|
}
|
|
375
383
|
|
|
376
384
|
return { index, packages, stale };
|
|
@@ -105,6 +105,10 @@ import { currentType } from "./ids.mjs";
|
|
|
105
105
|
* entry of the map is. A finding names the entry at fault rather than
|
|
106
106
|
* quoting the whole map back, so the string an author has to correct is the
|
|
107
107
|
* one the message holds.
|
|
108
|
+
* @property {string} [ref] - The type a bare value takes. An art slot declares
|
|
109
|
+
* one — `icon` for `icon` and `tokenIcon`, `image` for `bgImage` and
|
|
110
|
+
* `banner` — so `icon: anvil` names `icon-anvil` while a value carrying the
|
|
111
|
+
* separator states its own address.
|
|
108
112
|
* @property {"pack"} [keys] - For a `scalar-or-map` field, what its keys name.
|
|
109
113
|
* `"pack"` means each is a pack this package declares, so a key naming none
|
|
110
114
|
* is a finding of its own: it addresses a hierarchy nothing will ever read.
|
|
@@ -185,6 +189,57 @@ const TEMPLATE_PRIORITY = Object.freeze({
|
|
|
185
189
|
describe: "Template priority; unset means the note is not a template.",
|
|
186
190
|
});
|
|
187
191
|
|
|
192
|
+
/**
|
|
193
|
+
* What a token on the canvas wears — an Actor type's second piece of art.
|
|
194
|
+
*
|
|
195
|
+
* Declared once and shared by the two Actor types, beside
|
|
196
|
+
* {@link TEMPLATE_PRIORITY} and for the same reason. Unset it follows `icon`:
|
|
197
|
+
* a token has to read at grid scale and when a map is zoomed out, so it is an
|
|
198
|
+
* `icon` rather than an `image` and its fallback has to be `icon`-typed too.
|
|
199
|
+
*
|
|
200
|
+
* @type {DataFieldSpec}
|
|
201
|
+
*/
|
|
202
|
+
const TOKEN_ICON = Object.freeze({
|
|
203
|
+
name: "tokenIcon",
|
|
204
|
+
...LINK,
|
|
205
|
+
ref: "icon",
|
|
206
|
+
describe: "What a token on the canvas wears — an `icon` address; unset, it follows `icon`.",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The `data:` keys **every** note type accepts, whatever it is.
|
|
211
|
+
*
|
|
212
|
+
* `data:` is a closed container and the per-type vocabularies are the only
|
|
213
|
+
* lists there are, so a key legal on every type needs somewhere that is not one
|
|
214
|
+
* type's list — including the types whose own vocabulary is empty. Repeating a
|
|
215
|
+
* row in twenty-five tables would be twenty-five chances for one of them to
|
|
216
|
+
* disagree with the rest.
|
|
217
|
+
*
|
|
218
|
+
* Both are art slots, and they are legal everywhere for different reasons.
|
|
219
|
+
* `icon` is the document's profile art and most types compile into a document
|
|
220
|
+
* that carries one; where a type's passes emit none, the frontmatter lint says
|
|
221
|
+
* so as a warning rather than the vocabulary refusing the key, because the
|
|
222
|
+
* value may still be read by a page template. `banner` reaches no compiled
|
|
223
|
+
* document at all — it is the page's hero image — and a page is what every note
|
|
224
|
+
* publishes.
|
|
225
|
+
*
|
|
226
|
+
* @type {readonly DataFieldSpec[]}
|
|
227
|
+
*/
|
|
228
|
+
export const SHARED_DATA_FIELDS = Object.freeze([
|
|
229
|
+
Object.freeze({
|
|
230
|
+
name: "icon",
|
|
231
|
+
...LINK,
|
|
232
|
+
ref: "icon",
|
|
233
|
+
describe: "The document's profile art — an `icon` address, resolved into `img`.",
|
|
234
|
+
}),
|
|
235
|
+
Object.freeze({
|
|
236
|
+
name: "banner",
|
|
237
|
+
...LINK,
|
|
238
|
+
ref: "image",
|
|
239
|
+
describe: "The page's hero image — an `image` address. Reaches no compiled document.",
|
|
240
|
+
}),
|
|
241
|
+
]);
|
|
242
|
+
|
|
188
243
|
/**
|
|
189
244
|
* The four gear values every carried thing declares.
|
|
190
245
|
*
|
|
@@ -477,14 +532,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
477
532
|
// values here would put a second, weaker answer beside the real one.
|
|
478
533
|
subTypes: null,
|
|
479
534
|
data: Object.freeze([
|
|
480
|
-
|
|
481
|
-
name: "portrait",
|
|
482
|
-
...TEXT,
|
|
483
|
-
describe:
|
|
484
|
-
"Path to the portrait image. Its first segment says which package owns " +
|
|
485
|
-
"the file: `systems/…` and `modules/…` are emitted unchanged, anything " +
|
|
486
|
-
"else is this package's own and is rooted under its assets.",
|
|
487
|
-
},
|
|
535
|
+
TOKEN_ICON,
|
|
488
536
|
TEMPLATE_PRIORITY,
|
|
489
537
|
{ name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
|
|
490
538
|
{ name: "occupation", ...TEXT, describe: "What the being does for a living." },
|
|
@@ -527,17 +575,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
527
575
|
|
|
528
576
|
vehicle: Object.freeze({
|
|
529
577
|
subTypes: null,
|
|
530
|
-
data: Object.freeze([
|
|
531
|
-
{
|
|
532
|
-
name: "portrait",
|
|
533
|
-
...TEXT,
|
|
534
|
-
describe:
|
|
535
|
-
"Path to the portrait image. Its first segment says which package owns " +
|
|
536
|
-
"the file: `systems/…` and `modules/…` are emitted unchanged, anything " +
|
|
537
|
-
"else is this package's own and is rooted under its assets.",
|
|
538
|
-
},
|
|
539
|
-
TEMPLATE_PRIORITY,
|
|
540
|
-
]),
|
|
578
|
+
data: Object.freeze([TOKEN_ICON, TEMPLATE_PRIORITY]),
|
|
541
579
|
}),
|
|
542
580
|
|
|
543
581
|
/* ----- items ---------------------------------------------------- */
|
|
@@ -999,16 +1037,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
999
1037
|
// derived for them, which is precisely what a subType decides.
|
|
1000
1038
|
subTypes: Object.freeze(["battlemap", "localmap", "regionalmap"]),
|
|
1001
1039
|
data: Object.freeze([
|
|
1002
|
-
// `img`,
|
|
1003
|
-
//
|
|
1004
|
-
// and gone, so the two names are one again.
|
|
1040
|
+
// A Scene has no `img`, so the shared art key reaches nothing here:
|
|
1041
|
+
// a map's background is its own slot, and an `image` address.
|
|
1005
1042
|
{
|
|
1006
|
-
name: "
|
|
1007
|
-
...
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
"names — `systems/…` and `modules/…` unchanged, anything else this " +
|
|
1011
|
-
"package's own.",
|
|
1043
|
+
name: "bgImage",
|
|
1044
|
+
...LINK,
|
|
1045
|
+
ref: "image",
|
|
1046
|
+
describe: "The map's background art — an `image` address.",
|
|
1012
1047
|
},
|
|
1013
1048
|
{
|
|
1014
1049
|
name: "dimensions",
|
|
@@ -1150,12 +1185,19 @@ assertVocabularyCharset(NOTE_VOCABULARY);
|
|
|
1150
1185
|
* @param {string} type - The note's `type`.
|
|
1151
1186
|
* @param {Readonly<Record<string, TypeVocabulary>>} [vocabulary] - The registry
|
|
1152
1187
|
* to read, defaulting to {@link NOTE_VOCABULARY}.
|
|
1188
|
+
* {@link SHARED_DATA_FIELDS} come first, because they are part of every type's
|
|
1189
|
+
* declaration and a caller asking what a type accepts must be told all of it.
|
|
1190
|
+
*
|
|
1153
1191
|
* @returns {readonly DataFieldSpec[]|undefined} The declaration, or `undefined`
|
|
1154
1192
|
* when the type declares none — which is not the same as declaring an empty
|
|
1155
1193
|
* one, and is why the lint makes no claim rather than refusing every key.
|
|
1156
1194
|
*/
|
|
1157
1195
|
export function dataFields(type, vocabulary = NOTE_VOCABULARY) {
|
|
1158
|
-
|
|
1196
|
+
const entry = vocabulary?.[currentType(type)];
|
|
1197
|
+
if (!entry) return undefined;
|
|
1198
|
+
// The shared keys first, so a type's own declarations keep their authored
|
|
1199
|
+
// order behind them and a reader meets the art before the mechanics.
|
|
1200
|
+
return Object.freeze([...SHARED_DATA_FIELDS, ...(entry.data ?? [])]);
|
|
1159
1201
|
}
|
|
1160
1202
|
|
|
1161
1203
|
/**
|
package/engine/pack-router.mjs
CHANGED
|
@@ -371,6 +371,20 @@ export function createPackRouter(packs) {
|
|
|
371
371
|
return out;
|
|
372
372
|
},
|
|
373
373
|
|
|
374
|
+
/**
|
|
375
|
+
* The system one pack declares, if it declares one.
|
|
376
|
+
*
|
|
377
|
+
* A pack declaring none takes the compiler its document type falls
|
|
378
|
+
* back to, so `undefined` here is an answer rather than a gap: it says
|
|
379
|
+
* the pack's documents are whichever system that pass writes.
|
|
380
|
+
*
|
|
381
|
+
* @param {string} name - The pack name.
|
|
382
|
+
* @returns {string|undefined} Its `system:`, or `undefined`.
|
|
383
|
+
*/
|
|
384
|
+
systemOf(name) {
|
|
385
|
+
return byName.get(name)?.system || undefined;
|
|
386
|
+
},
|
|
387
|
+
|
|
374
388
|
/**
|
|
375
389
|
* The pack of a type that receives notes declaring none.
|
|
376
390
|
*
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
* The toolchain's own content index — the files it ships, addressed.
|
|
16
|
+
*
|
|
17
|
+
* package-build ships a set of images, section banners chiefly, that many
|
|
18
|
+
* packages draw on. `packagebuild-none-image-<shortcode>` lets a note reach one
|
|
19
|
+
* without declaring a dependency on some parent system or module it otherwise
|
|
20
|
+
* has no relationship with, which is the whole point: the alternative is every
|
|
21
|
+
* package depending on one of the others just to borrow a banner.
|
|
22
|
+
*
|
|
23
|
+
* **Every other package's index is fetched; this one is read from disk.**
|
|
24
|
+
* package-build is an npm dependency of every consumer rather than a Foundry
|
|
25
|
+
* package, so there is no release archive to fetch and no reason to fetch one —
|
|
26
|
+
* `node_modules/@heroiclands/package-build/assets/` is already there. That is
|
|
27
|
+
* the one difference, and it is entirely about *acquisition*: the records join
|
|
28
|
+
* `foreign.index` like any other package's, so every lookup stays one path.
|
|
29
|
+
*
|
|
30
|
+
* **A cold cache is not a failure mode here**, and the walk is why. The records
|
|
31
|
+
* are derived from the shipped tree each time they are asked for rather than
|
|
32
|
+
* read back from a file, so an installed copy and a git checkout answer alike
|
|
33
|
+
* and nothing can be half-fetched. {@link emitPackageBuildIndex} publishes the
|
|
34
|
+
* same walk as a file for the readers that are not this process — the website,
|
|
35
|
+
* the book, the Foundry runtime — and a guard holds the two together.
|
|
36
|
+
*
|
|
37
|
+
* **These addresses have no Foundry form, by construction.** Foundry installs
|
|
38
|
+
* no package for this one, so `packageAddresses` has no entry and
|
|
39
|
+
* `resolvePathname` yields `null` rather than deriving
|
|
40
|
+
* `modules/packagebuild/assets/…`, which installs nowhere. The only slot that
|
|
41
|
+
* names them is `banner`, which reaches no compiled document at all.
|
|
42
|
+
*
|
|
43
|
+
* @module
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import fs from "node:fs";
|
|
47
|
+
import path from "node:path";
|
|
48
|
+
import { fileURLToPath } from "node:url";
|
|
49
|
+
|
|
50
|
+
import { collectAssetRecords } from "./asset-index.mjs";
|
|
51
|
+
import { recordPath, sortKeysDeep } from "./index-records.mjs";
|
|
52
|
+
import { PACKAGEBUILD_PACKAGE, metadataFileName } from "./packages.mjs";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* This package's own root, wherever it is installed.
|
|
56
|
+
*
|
|
57
|
+
* Derived from this module's location rather than from a configuration: the
|
|
58
|
+
* files being indexed sit beside it in the same tarball, so the one reliable
|
|
59
|
+
* answer is "up from here". A consuming repository's `rootDir` names its own
|
|
60
|
+
* tree and says nothing about where its dependencies live.
|
|
61
|
+
*
|
|
62
|
+
* @type {string}
|
|
63
|
+
*/
|
|
64
|
+
export const PACKAGEBUILD_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The asset directory this package ships.
|
|
68
|
+
*
|
|
69
|
+
* @type {string}
|
|
70
|
+
*/
|
|
71
|
+
export const PACKAGEBUILD_ASSETS = path.join(PACKAGEBUILD_ROOT, "assets");
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Where the published index sits, beside the files it describes.
|
|
75
|
+
*
|
|
76
|
+
* @type {string}
|
|
77
|
+
*/
|
|
78
|
+
export const PACKAGEBUILD_INDEX_FILE = path.join(
|
|
79
|
+
PACKAGEBUILD_ROOT,
|
|
80
|
+
metadataFileName(PACKAGEBUILD_PACKAGE),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The index records for the files this package ships.
|
|
85
|
+
*
|
|
86
|
+
* Walked rather than read back, so an installed copy and a git checkout answer
|
|
87
|
+
* alike and there is no state to be stale. The tree is small — a handful of
|
|
88
|
+
* banners — so the walk costs nothing a build would notice.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} [assetsBase] - The asset directory, for a caller testing
|
|
91
|
+
* against a tree of its own.
|
|
92
|
+
* @returns {Array<Record<string, any>>} One record per addressable file.
|
|
93
|
+
*/
|
|
94
|
+
export function packageBuildRecords(assetsBase = PACKAGEBUILD_ASSETS) {
|
|
95
|
+
const records = collectAssetRecords(assetsBase, {
|
|
96
|
+
contentPackage: PACKAGEBUILD_PACKAGE,
|
|
97
|
+
}).map((record) => /** @type {Record<string, any>} */ (sortKeysDeep(record)));
|
|
98
|
+
// Ordered and key-sorted like every other index, and for the same reason:
|
|
99
|
+
// directory-read order is a fact about the filesystem, so two runs over an
|
|
100
|
+
// unchanged tree have to produce an identical file.
|
|
101
|
+
records.sort((a, b) => recordPath(a).localeCompare(recordPath(b), "en"));
|
|
102
|
+
return records;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Publish the index as a file, for the readers that are not this process.
|
|
107
|
+
*
|
|
108
|
+
* The website, the book and the Foundry runtime each read a package's index as
|
|
109
|
+
* JSON Lines; this package's has to be one too, and it ships in the tarball
|
|
110
|
+
* beside the images it describes.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} [options] - Options.
|
|
113
|
+
* @param {string} [options.assetsBase] - The asset directory to walk.
|
|
114
|
+
* @param {string} [options.file] - Where to write it.
|
|
115
|
+
* @returns {{file: string, assets: number, bytes: number}} Where it was
|
|
116
|
+
* written, how many files it holds, and its size.
|
|
117
|
+
*/
|
|
118
|
+
export function emitPackageBuildIndex({ assetsBase, file = PACKAGEBUILD_INDEX_FILE } = {}) {
|
|
119
|
+
const records = packageBuildRecords(assetsBase);
|
|
120
|
+
const text = records.length ? `${records.map((r) => JSON.stringify(r)).join("\n")}\n` : "";
|
|
121
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
122
|
+
fs.writeFileSync(file, text);
|
|
123
|
+
return { file, assets: records.length, bytes: Buffer.byteLength(text) };
|
|
124
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
* The `<package>` segment's own registry: the names no repository may claim.
|
|
16
|
+
*
|
|
17
|
+
* A content package names itself, so the registry is open — and exactly one
|
|
18
|
+
* name is held back from it.
|
|
19
|
+
*
|
|
20
|
+
* **`packagebuild` is a package that is not a package.** package-build is an npm
|
|
21
|
+
* package rather than a system or a module, and it ships a set of images —
|
|
22
|
+
* section banners chiefly — that many packages draw on. Addressing them as
|
|
23
|
+
* `packagebuild-none-image-<shortcode>` lets a note reach one without declaring a
|
|
24
|
+
* dependency on some parent system or module it otherwise has no relationship
|
|
25
|
+
* with, which is the whole point: the alternative is every package depending on
|
|
26
|
+
* one of the others just to borrow a banner.
|
|
27
|
+
*
|
|
28
|
+
* So nothing may create a real package that collides with the name, and its
|
|
29
|
+
* resolution is special-cased, because no installed directory sits behind it.
|
|
30
|
+
*
|
|
31
|
+
* This module is a **leaf with no local imports**, so the configuration
|
|
32
|
+
* validator can name it without closing a cycle around `content-config.mjs`.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The address namespace package-build's own assets publish under.
|
|
39
|
+
*
|
|
40
|
+
* @type {string}
|
|
41
|
+
*/
|
|
42
|
+
export const PACKAGEBUILD_PACKAGE = "packagebuild";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every package name a repository may not claim.
|
|
46
|
+
*
|
|
47
|
+
* @type {ReadonlySet<string>}
|
|
48
|
+
*/
|
|
49
|
+
export const RESERVED_PACKAGES = Object.freeze(new Set([PACKAGEBUILD_PACKAGE]));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What a package's content index is called, wherever it is written or fetched.
|
|
53
|
+
*
|
|
54
|
+
* **The local index and the published artifact are one file.** A package emits
|
|
55
|
+
* this, ships it as a release asset, and advertises it as `flags.metadataUrl`;
|
|
56
|
+
* a consumer fetches that same file into its cache and reads it. Naming it in
|
|
57
|
+
* one function is what keeps the emitter, the release and the fetcher from
|
|
58
|
+
* drifting into three spellings of one artifact.
|
|
59
|
+
*
|
|
60
|
+
* The `-metadata` suffix earns its place: a bare `<package>.jsonl` says nothing
|
|
61
|
+
* about what it holds, and these files land in a cache directory beside other
|
|
62
|
+
* packages' artifacts where the name is all a reader has.
|
|
63
|
+
*
|
|
64
|
+
* It lives here rather than beside the loader because the loader reaches the
|
|
65
|
+
* toolchain's own index, which has to name its file without importing the
|
|
66
|
+
* loader back.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} pkg - The content package name.
|
|
69
|
+
* @returns {string} The file name, e.g. `sohl-metadata.jsonl`.
|
|
70
|
+
*/
|
|
71
|
+
export function metadataFileName(pkg) {
|
|
72
|
+
return `${pkg}-metadata.jsonl`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether a package name is held back from the open registry.
|
|
77
|
+
*
|
|
78
|
+
* @param {unknown} pkg - The candidate `contentPackage`.
|
|
79
|
+
* @returns {boolean} True when the name is reserved.
|
|
80
|
+
*/
|
|
81
|
+
export function isReservedPackage(pkg) {
|
|
82
|
+
return typeof pkg === "string" && RESERVED_PACKAGES.has(pkg.toLowerCase());
|
|
83
|
+
}
|