@heroiclands/package-build 0.6.1 → 3.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-content-build.md +965 -0
- package/CHANGELOG.md +58 -0
- package/CONTENT.md +824 -0
- package/MIGRATING.md +127 -0
- package/README.md +29 -17
- package/bin/content-build.mjs +1082 -0
- package/bin/package-build.mjs +2 -2
- package/bin/report.mjs +2 -2
- package/config.mjs +21 -18
- package/content-config.mjs +1424 -0
- package/coverage.mjs +1 -1
- package/e2e.mjs +1 -1
- package/engine/abbreviations.mjs +0 -0
- package/engine/base-compiler.mjs +608 -0
- package/engine/code-fences.mjs +311 -0
- package/engine/compendiums.mjs +353 -0
- package/engine/content-address.mjs +141 -0
- package/engine/content-links.mjs +438 -0
- package/engine/content-lint.mjs +204 -0
- package/engine/content-package.mjs +67 -0
- package/engine/content-slug.mjs +169 -0
- package/engine/content-tables.mjs +1216 -0
- package/engine/content-tree.mjs +56 -0
- package/engine/diagnostics.mjs +270 -0
- package/engine/field-reference.mjs +242 -0
- package/engine/field-spec.mjs +218 -0
- package/engine/foreign-catalog.mjs +450 -0
- package/engine/foreign-manifests.mjs +129 -0
- package/engine/frontmatter-lint.mjs +343 -0
- package/engine/frontmatter.mjs +258 -0
- package/engine/generate.mjs +305 -0
- package/engine/helpers.mjs +809 -0
- package/engine/ids.mjs +192 -0
- package/engine/index.mjs +142 -0
- package/engine/item-docs.mjs +153 -0
- package/engine/item-registry.mjs +129 -0
- package/engine/journals.mjs +374 -0
- package/engine/kb-manifest.mjs +473 -0
- package/engine/macros.mjs +333 -0
- package/engine/manifest-emit.mjs +339 -0
- package/engine/map-notes.mjs +1302 -0
- package/engine/pack-config.mjs +479 -0
- package/engine/pack-router.mjs +254 -0
- package/engine/prose-config.mjs +238 -0
- package/engine/prose-lint.mjs +294 -0
- package/engine/region-events.mjs +79 -0
- package/engine/scene-levels.mjs +420 -0
- package/engine/scenes.mjs +528 -0
- package/engine/site-build.mjs +778 -0
- package/engine/site-index.mjs +332 -0
- package/engine/web-wikilinks.mjs +330 -0
- package/engine/wikilink-syntax.mjs +105 -0
- package/engine/wikilinks.mjs +645 -0
- package/index.mjs +42 -25
- package/lang.mjs +1 -1
- package/manifest.mjs +79 -5
- package/markdownlint-config.mjs +45 -0
- package/package.json +78 -35
- package/prettier-config.mjs +34 -0
- package/sohl/actors.mjs +581 -0
- package/sohl/affiliation-standings.mjs +44 -0
- package/sohl/being-info.mjs +173 -0
- package/sohl/default-item-art.mjs +82 -0
- package/sohl/index.mjs +55 -0
- package/sohl/item-builders.mjs +114 -0
- package/sohl/item-fields.mjs +867 -0
- package/sohl/items.mjs +197 -0
- package/sohl/kb-passes.mjs +207 -0
- package/sohl/note-schemas.mjs +322 -0
- package/templates.mjs +1 -1
- package/types/config.d.mts +1 -1
- package/types/content-config.d.mts +571 -0
- package/types/engine/abbreviations.d.mts +44 -0
- package/types/engine/base-compiler.d.mts +320 -0
- package/types/engine/code-fences.d.mts +102 -0
- package/types/engine/compendiums.d.mts +77 -0
- package/types/engine/content-address.d.mts +61 -0
- package/types/engine/content-links.d.mts +87 -0
- package/types/engine/content-lint.d.mts +47 -0
- package/types/engine/content-package.d.mts +36 -0
- package/types/engine/content-slug.d.mts +59 -0
- package/types/engine/content-tables.d.mts +131 -0
- package/types/engine/content-tree.d.mts +12 -0
- package/types/engine/diagnostics.d.mts +152 -0
- package/types/engine/field-reference.d.mts +20 -0
- package/types/engine/field-spec.d.mts +181 -0
- package/types/engine/foreign-catalog.d.mts +103 -0
- package/types/engine/foreign-manifests.d.mts +43 -0
- package/types/engine/frontmatter-lint.d.mts +74 -0
- package/types/engine/frontmatter.d.mts +99 -0
- package/types/engine/generate.d.mts +56 -0
- package/types/engine/helpers.d.mts +348 -0
- package/types/engine/ids.d.mts +126 -0
- package/types/engine/index.d.mts +33 -0
- package/types/engine/item-docs.d.mts +73 -0
- package/types/engine/item-registry.d.mts +57 -0
- package/types/engine/journals.d.mts +121 -0
- package/types/engine/kb-manifest.d.mts +214 -0
- package/types/engine/macros.d.mts +201 -0
- package/types/engine/manifest-emit.d.mts +137 -0
- package/types/engine/map-notes.d.mts +283 -0
- package/types/engine/pack-config.d.mts +77 -0
- package/types/engine/pack-router.d.mts +53 -0
- package/types/engine/prose-config.d.mts +121 -0
- package/types/engine/prose-lint.d.mts +59 -0
- package/types/engine/region-events.d.mts +43 -0
- package/types/engine/scene-levels.d.mts +36 -0
- package/types/engine/scenes.d.mts +48 -0
- package/types/engine/site-build.d.mts +214 -0
- package/types/engine/site-index.d.mts +141 -0
- package/types/engine/web-wikilinks.d.mts +67 -0
- package/types/engine/wikilink-syntax.d.mts +101 -0
- package/types/engine/wikilinks.d.mts +172 -0
- package/types/index.d.mts +3 -0
- package/types/manifest.d.mts +52 -4
- package/types/markdownlint-config.d.mts +8 -0
- package/types/prettier-config.d.mts +1 -0
- package/types/sohl/actors.d.mts +65 -0
- package/types/sohl/affiliation-standings.d.mts +22 -0
- package/types/sohl/being-info.d.mts +78 -0
- package/types/sohl/default-item-art.d.mts +28 -0
- package/types/sohl/index.d.mts +7 -0
- package/types/sohl/item-builders.d.mts +11 -0
- package/types/sohl/item-fields.d.mts +20 -0
- package/types/sohl/items.d.mts +19 -0
- package/types/sohl/kb-passes.d.mts +71 -0
- package/types/sohl/note-schemas.d.mts +7 -0
|
@@ -0,0 +1,420 @@
|
|
|
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
|
+
* **Scene ↔ Level integrity** for a compiled compendium pack (issue #1538).
|
|
16
|
+
*
|
|
17
|
+
* A v14 Scene keeps its map image on an embedded `Level`, and a compiled pack
|
|
18
|
+
* stores the two in *separate* LevelDB keys: the Scene at `!scenes!<id>`
|
|
19
|
+
* holding `levels` as an array of ids, and each Level at
|
|
20
|
+
* `!scenes.levels!<sceneId>.<levelId>`. Nothing in Foundry ties them together
|
|
21
|
+
* on read. If a Level record is missing, `EmbeddedCollectionField#expandEmbedded`
|
|
22
|
+
* merely warns
|
|
23
|
+
*
|
|
24
|
+
* > _N embedded levels records in Level `<sceneId>` were undefined and not
|
|
25
|
+
* > retrieved from the scenes.levels sublevel_
|
|
26
|
+
*
|
|
27
|
+
* and yields an empty collection. The very next world launch migrates that
|
|
28
|
+
* scene, **persists `levels: []`**, and leaves `initialLevel` pointing at an id
|
|
29
|
+
* that no longer exists — measured on both 14.359 and 14.367. The map image is
|
|
30
|
+
* gone for good, and the only symptom a human sees is a blank battlemap.
|
|
31
|
+
*
|
|
32
|
+
* Foundry is behaving correctly there: a scene with no Level records genuinely
|
|
33
|
+
* has no levels. The damage is that the condition is *unobservable* until it is
|
|
34
|
+
* permanent. So the pack build asserts the invariant on the artefact it just
|
|
35
|
+
* wrote — the compiled LevelDB, not the JSON it was compiled from — because the
|
|
36
|
+
* gap this closes is the write path (the emitter is already unit-tested, and
|
|
37
|
+
* the compendium CLI has previously mishandled Scene Levels).
|
|
38
|
+
*
|
|
39
|
+
* An {@link https://foundryvtt.com/api/classes/foundry.documents.BaseAdventure.html Adventure}
|
|
40
|
+
* carries its scenes *inline*, levels and all, so the same invariant has a
|
|
41
|
+
* second shape and a second way to ship a mapless map; both are checked here.
|
|
42
|
+
*
|
|
43
|
+
* Plain ESM with no Foundry, so the rule itself is a pure function over records
|
|
44
|
+
* and is unit-tested directly.
|
|
45
|
+
*
|
|
46
|
+
* **What a report has to say (#9).** The likeliest way to meet any of this is a
|
|
47
|
+
* compendium CLI older than {@link LEVEL_SPLITTING_CLI}, which does not split
|
|
48
|
+
* Scene Levels into the sublevel at all — so the guard fires on every scene at
|
|
49
|
+
* once, and each `levels` entry is an inline Level *object* rather than an id.
|
|
50
|
+
* Two rules keep that legible: an entry is named by the **shape it has** (an
|
|
51
|
+
* object interpolated into a key reads `[object Object]` and names a key that
|
|
52
|
+
* could not exist), and a pack with **no** `!scenes.levels!` records at all is
|
|
53
|
+
* one fact about the compile, reported once, not once per scene.
|
|
54
|
+
*
|
|
55
|
+
* @module
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { createRequire } from "node:module";
|
|
59
|
+
|
|
60
|
+
import { ClassicLevel } from "classic-level";
|
|
61
|
+
|
|
62
|
+
/** LevelDB key prefix for a pack's primary Scene records. */
|
|
63
|
+
const SCENE_PREFIX = "!scenes!";
|
|
64
|
+
|
|
65
|
+
/** LevelDB key prefix for the `levels` sublevel of those Scene records. */
|
|
66
|
+
const LEVEL_PREFIX = "!scenes.levels!";
|
|
67
|
+
|
|
68
|
+
/** LevelDB key prefix for a pack's Adventure records. */
|
|
69
|
+
const ADVENTURE_PREFIX = "!adventures!";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The compendium CLI release that began splitting Scene Levels into the
|
|
73
|
+
* `!scenes.levels!` sublevel. Anything older writes them inline on the Scene
|
|
74
|
+
* document, which is the shape the reports below name (#9).
|
|
75
|
+
*/
|
|
76
|
+
const LEVEL_SPLITTING_CLI = "3.0.3";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The version of `@foundryvtt/foundryvtt-cli` this build actually resolves.
|
|
80
|
+
*
|
|
81
|
+
* Resolved from *this* module, so it names the copy the pack compile runs on
|
|
82
|
+
* rather than whatever a shell happens to find. The installed version is the
|
|
83
|
+
* write path, so it is the fact a report should carry.
|
|
84
|
+
*
|
|
85
|
+
* @returns {string | undefined} The resolved version, or `undefined` when the
|
|
86
|
+
* package cannot be read (a pruned install, an exports-restricted copy).
|
|
87
|
+
*/
|
|
88
|
+
export function compendiumCliVersion() {
|
|
89
|
+
try {
|
|
90
|
+
return createRequire(import.meta.url)(
|
|
91
|
+
"@foundryvtt/foundryvtt-cli/package.json",
|
|
92
|
+
).version;
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Is a `levels` entry an inline Level document rather than an id?
|
|
100
|
+
*
|
|
101
|
+
* @param {unknown} entry - One entry of a scene's `levels` array.
|
|
102
|
+
* @returns {boolean} True for an object, which on a compiled `!scenes!` record
|
|
103
|
+
* is always the wrong shape.
|
|
104
|
+
*/
|
|
105
|
+
function isInlineLevel(entry) {
|
|
106
|
+
return typeof entry === "object" && entry !== null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The Level id an entry names, in whichever shape it named it.
|
|
111
|
+
*
|
|
112
|
+
* @param {unknown} entry - One entry of a scene's `levels` array.
|
|
113
|
+
* @returns {string | undefined} The id, or `undefined` when it names none.
|
|
114
|
+
*/
|
|
115
|
+
function levelIdOf(entry) {
|
|
116
|
+
if (typeof entry === "string" && entry) return entry;
|
|
117
|
+
if (isInlineLevel(entry) && typeof entry._id === "string" && entry._id) {
|
|
118
|
+
return entry._id;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Name a `levels` entry by the shape it actually has.
|
|
125
|
+
*
|
|
126
|
+
* @param {unknown} entry - One entry of a scene's `levels` array.
|
|
127
|
+
* @param {number} index - Its position, which the report cites.
|
|
128
|
+
* @returns {string} A phrase naming the entry, never `[object Object]`.
|
|
129
|
+
*/
|
|
130
|
+
function describeEntry(entry, index) {
|
|
131
|
+
if (isInlineLevel(entry))
|
|
132
|
+
return `\`levels[${index}]\` is ${describeShape(entry)}`;
|
|
133
|
+
return `\`levels[${index}]\` is ${JSON.stringify(entry) ?? String(entry)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Name an inline Level object by its `_id`, the fact that identifies it.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} entry - An inline Level object.
|
|
140
|
+
* @returns {string} A phrase naming it, never `[object Object]`.
|
|
141
|
+
*/
|
|
142
|
+
function describeShape(entry) {
|
|
143
|
+
const id = levelIdOf(entry);
|
|
144
|
+
return `an inline Level object (\`_id\`: ${id ? `"${id}"` : "absent"})`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The `levels` a scene declares, in whatever shape it declared them.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} scene - The scene document.
|
|
151
|
+
* @returns {unknown[]} Its declared entries, empty when it declares none.
|
|
152
|
+
*/
|
|
153
|
+
function declaredLevels(scene) {
|
|
154
|
+
return Array.isArray(scene?.levels) ? scene.levels : [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* How to name a scene in a problem report.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} sceneId - The scene's id.
|
|
161
|
+
* @param {object} scene - The scene document.
|
|
162
|
+
* @returns {string} A phrase naming it by name and id.
|
|
163
|
+
*/
|
|
164
|
+
function nameScene(sceneId, scene) {
|
|
165
|
+
return `Scene "${scene?.name ?? sceneId}" [${sceneId}]`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Does a compendium CLI version predate {@link LEVEL_SPLITTING_CLI}?
|
|
170
|
+
*
|
|
171
|
+
* @param {string | undefined} version - A resolved version, if one is known.
|
|
172
|
+
* @returns {boolean | undefined} Whether it predates the split, or `undefined`
|
|
173
|
+
* when there is no version to judge or it does not parse.
|
|
174
|
+
*/
|
|
175
|
+
function predatesLevelSplitting(version) {
|
|
176
|
+
const parse = (value) => String(value).split(/[.+-]/, 3).map(Number);
|
|
177
|
+
const found = parse(version);
|
|
178
|
+
if (found.length < 3 || found.some((part) => !Number.isInteger(part))) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const want = parse(LEVEL_SPLITTING_CLI);
|
|
182
|
+
for (const [index, part] of found.entries()) {
|
|
183
|
+
if (part !== want[index]) return part < want[index];
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* What to do about it, decided from the version actually resolved — so the
|
|
190
|
+
* report ends in an instruction rather than a lead to follow (#9).
|
|
191
|
+
*
|
|
192
|
+
* @param {string | undefined} cliVersion - The resolved compendium CLI
|
|
193
|
+
* version, when it is known.
|
|
194
|
+
* @returns {string} A sentence naming the next step.
|
|
195
|
+
*/
|
|
196
|
+
function describeRemedy(cliVersion) {
|
|
197
|
+
const older = predatesLevelSplitting(cliVersion);
|
|
198
|
+
if (older === true) {
|
|
199
|
+
return (
|
|
200
|
+
`The resolved \`@foundryvtt/foundryvtt-cli\` is ${cliVersion}, ` +
|
|
201
|
+
`which predates ${LEVEL_SPLITTING_CLI}: install ` +
|
|
202
|
+
`${LEVEL_SPLITTING_CLI} or newer and recompile.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (older === false) {
|
|
206
|
+
return (
|
|
207
|
+
`The resolved \`@foundryvtt/foundryvtt-cli\` is ${cliVersion}, ` +
|
|
208
|
+
`which does split them — so check ` +
|
|
209
|
+
`\`npm ls @foundryvtt/foundryvtt-cli\` for a second, older copy, ` +
|
|
210
|
+
`then recompile.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return (
|
|
214
|
+
`Check the installed \`@foundryvtt/foundryvtt-cli\` ` +
|
|
215
|
+
`(\`npm ls @foundryvtt/foundryvtt-cli\`; ${LEVEL_SPLITTING_CLI} or ` +
|
|
216
|
+
`newer is required) and recompile.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The pack declares Levels and holds **no** `!scenes.levels!` records at all.
|
|
222
|
+
*
|
|
223
|
+
* That is one fact about the compile, not one per scene, so it is reported
|
|
224
|
+
* once with every affected scene named. A pack that lost a single record lost
|
|
225
|
+
* it on that record's write path; a pack that has none never split them, which
|
|
226
|
+
* is what a compendium CLI older than {@link LEVEL_SPLITTING_CLI} does — two
|
|
227
|
+
* different diagnoses, so the message names whichever shape it found rather
|
|
228
|
+
* than guessing (#9).
|
|
229
|
+
*
|
|
230
|
+
* @param {Array<[string, object]>} declaring - `[sceneId, scene]` for every
|
|
231
|
+
* scene declaring at least one level.
|
|
232
|
+
* @param {string | undefined} cliVersion - The resolved compendium CLI
|
|
233
|
+
* version, when it is known.
|
|
234
|
+
* @returns {string} One report covering every affected scene.
|
|
235
|
+
*/
|
|
236
|
+
function describeWholesaleLoss(declaring, cliVersion) {
|
|
237
|
+
const inline = declaring.some(([, scene]) =>
|
|
238
|
+
declaredLevels(scene).some(isInlineLevel),
|
|
239
|
+
);
|
|
240
|
+
const list = declaring
|
|
241
|
+
.map(([sceneId, scene]) => {
|
|
242
|
+
const entries = declaredLevels(scene)
|
|
243
|
+
.map((entry) =>
|
|
244
|
+
isInlineLevel(entry) ? describeShape(entry) : `"${entry}"`,
|
|
245
|
+
)
|
|
246
|
+
.join(", ");
|
|
247
|
+
return `${nameScene(sceneId, scene)} → ${entries}`;
|
|
248
|
+
})
|
|
249
|
+
.join("; ");
|
|
250
|
+
const cause =
|
|
251
|
+
inline ?
|
|
252
|
+
`Those entries are inline Level objects rather than ids, which is ` +
|
|
253
|
+
`what \`@foundryvtt/foundryvtt-cli\` older than ` +
|
|
254
|
+
`${LEVEL_SPLITTING_CLI} writes: it does not split Scene Levels ` +
|
|
255
|
+
`into the ${LEVEL_PREFIX} sublevel.`
|
|
256
|
+
: `The whole ${LEVEL_PREFIX} sublevel is missing rather than one ` +
|
|
257
|
+
`record, so the fault is in the compile step and not in any one ` +
|
|
258
|
+
`Scene.`;
|
|
259
|
+
const count = declaring.length;
|
|
260
|
+
return (
|
|
261
|
+
`The compiled pack has no ${LEVEL_PREFIX} records at all, but ` +
|
|
262
|
+
`${count} Scene${count === 1 ? "" : "s"} ` +
|
|
263
|
+
`declare${count === 1 ? "s" : ""} Levels — ${list} — so every one of ` +
|
|
264
|
+
`those map images is lost. ${cause} ${describeRemedy(cliVersion)}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check the `levels` a scene declares, whatever shape it declared them in.
|
|
270
|
+
*
|
|
271
|
+
* Each violation is reported once, at its most specific: a level id whose
|
|
272
|
+
* record is missing is reported by the caller, and does not also count as the
|
|
273
|
+
* scene "having no Level" — one broken fact, one message.
|
|
274
|
+
*
|
|
275
|
+
* @param {object} scene - The scene document.
|
|
276
|
+
* @param {string[]} levelIds - The Level ids the scene declares.
|
|
277
|
+
* @param {string} where - How to name the scene in a problem report.
|
|
278
|
+
* @returns {string[]} A problem per broken rule; empty when the scene is sound.
|
|
279
|
+
*/
|
|
280
|
+
function checkDeclaredLevels(scene, levelIds, where) {
|
|
281
|
+
if (!levelIds.length) {
|
|
282
|
+
return [
|
|
283
|
+
`${where} has no Level — its map image cannot be stored, and ` +
|
|
284
|
+
`Foundry will persist \`levels: []\` on the next world launch.`,
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
const initial = scene.initialLevel;
|
|
288
|
+
if (initial && !levelIds.includes(initial)) {
|
|
289
|
+
return [
|
|
290
|
+
`${where} names initialLevel "${initial}", which is not one of ` +
|
|
291
|
+
`its levels (${levelIds.join(", ")}) — a dangling reference.`,
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Every way a compiled pack can ship a Scene that has lost its Level.
|
|
299
|
+
*
|
|
300
|
+
* @param {Iterable<[string, object]>} records - `[key, value]` pairs from a
|
|
301
|
+
* compiled pack's LevelDB, in any order.
|
|
302
|
+
* @param {object} [options] - Reporting context.
|
|
303
|
+
* @param {string} [options.cliVersion] - The resolved compendium CLI version,
|
|
304
|
+
* which decides what a wholesale loss is blamed on. Defaults to unknown,
|
|
305
|
+
* which leaves the report to say only what it can prove.
|
|
306
|
+
* @returns {string[]} One human-readable problem per violation, empty when the
|
|
307
|
+
* pack is sound.
|
|
308
|
+
*/
|
|
309
|
+
export function checkSceneLevels(records, { cliVersion } = {}) {
|
|
310
|
+
/** @type {Array<[string, object]>} `!scenes!` records, by key. */
|
|
311
|
+
const scenes = [];
|
|
312
|
+
/** @type {Set<string>} `<sceneId>.<levelId>` for every sublevel record. */
|
|
313
|
+
const levelKeys = new Set();
|
|
314
|
+
/** @type {Array<object>} `!adventures!` records. */
|
|
315
|
+
const adventures = [];
|
|
316
|
+
|
|
317
|
+
for (const [key, value] of records) {
|
|
318
|
+
if (key.startsWith(LEVEL_PREFIX)) {
|
|
319
|
+
levelKeys.add(key.slice(LEVEL_PREFIX.length));
|
|
320
|
+
} else if (key.startsWith(SCENE_PREFIX)) {
|
|
321
|
+
scenes.push([key.slice(SCENE_PREFIX.length), value]);
|
|
322
|
+
} else if (key.startsWith(ADVENTURE_PREFIX)) {
|
|
323
|
+
adventures.push(value);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const problems = [];
|
|
328
|
+
|
|
329
|
+
// No scene in the pack has a Level record: one fact about the compile,
|
|
330
|
+
// which saying per scene would bury. Scenes declaring nothing are not
|
|
331
|
+
// covered by it, so they are still judged individually below.
|
|
332
|
+
const declaring = scenes.filter(
|
|
333
|
+
([, scene]) => declaredLevels(scene).length,
|
|
334
|
+
);
|
|
335
|
+
const wholesale = levelKeys.size === 0 && declaring.length > 0;
|
|
336
|
+
if (wholesale) problems.push(describeWholesaleLoss(declaring, cliVersion));
|
|
337
|
+
|
|
338
|
+
for (const [sceneId, scene] of scenes) {
|
|
339
|
+
const declared = declaredLevels(scene);
|
|
340
|
+
if (wholesale && declared.length) continue;
|
|
341
|
+
const where = nameScene(sceneId, scene);
|
|
342
|
+
|
|
343
|
+
// Judge each entry by the shape it has: a compiled `!scenes!` record
|
|
344
|
+
// stores ids, so an inline Level object is itself the defect, and
|
|
345
|
+
// naming it is what identifies the cause.
|
|
346
|
+
const broken = [];
|
|
347
|
+
declared.forEach((entry, index) => {
|
|
348
|
+
const id = levelIdOf(entry);
|
|
349
|
+
if (isInlineLevel(entry)) {
|
|
350
|
+
broken.push(
|
|
351
|
+
`${where}: ${describeEntry(entry, index)}, not an id — a ` +
|
|
352
|
+
`compiled pack stores each Level as its own ` +
|
|
353
|
+
`${LEVEL_PREFIX}<sceneId>.<levelId> record, and none ` +
|
|
354
|
+
`exists at ${LEVEL_PREFIX}${sceneId}.${id ?? "<id>"}. ` +
|
|
355
|
+
`Inline Levels are what ` +
|
|
356
|
+
`\`@foundryvtt/foundryvtt-cli\` older than ` +
|
|
357
|
+
`${LEVEL_SPLITTING_CLI} writes.`,
|
|
358
|
+
);
|
|
359
|
+
} else if (!id) {
|
|
360
|
+
broken.push(
|
|
361
|
+
`${where}: ${describeEntry(entry, index)}, not a Level id.`,
|
|
362
|
+
);
|
|
363
|
+
} else if (!levelKeys.has(`${sceneId}.${id}`)) {
|
|
364
|
+
broken.push(
|
|
365
|
+
`${where} lists level "${id}", but no record exists at ` +
|
|
366
|
+
`${LEVEL_PREFIX}${sceneId}.${id} — the map image is ` +
|
|
367
|
+
`lost.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// A broken entry is already reported above; only the declaration
|
|
373
|
+
// itself is judged here, so nothing is reported twice.
|
|
374
|
+
if (broken.length) problems.push(...broken);
|
|
375
|
+
else
|
|
376
|
+
problems.push(...checkDeclaredLevels(scene ?? {}, declared, where));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const adventure of adventures) {
|
|
380
|
+
const inline = Array.isArray(adventure?.scenes) ? adventure.scenes : [];
|
|
381
|
+
for (const scene of inline) {
|
|
382
|
+
const levelIds = declaredLevels(scene).map(
|
|
383
|
+
(level) => levelIdOf(level) ?? level,
|
|
384
|
+
);
|
|
385
|
+
const where =
|
|
386
|
+
`Adventure "${adventure?.name ?? adventure?._id}" scene ` +
|
|
387
|
+
`"${scene?.name ?? scene?._id}"`;
|
|
388
|
+
problems.push(...checkDeclaredLevels(scene ?? {}, levelIds, where));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return problems;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Read a compiled pack back off disk and check it.
|
|
397
|
+
*
|
|
398
|
+
* The pack is opened after the compendium CLI has closed it, so this reads the
|
|
399
|
+
* bytes that will actually ship rather than the JSON they were compiled from.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} packDir - Directory of the compiled LevelDB pack.
|
|
402
|
+
* @returns {Promise<string[]>} The problems found, empty when the pack is sound.
|
|
403
|
+
*/
|
|
404
|
+
export async function verifyPackSceneLevels(packDir) {
|
|
405
|
+
const db = new ClassicLevel(packDir, {
|
|
406
|
+
keyEncoding: "utf8",
|
|
407
|
+
valueEncoding: "json",
|
|
408
|
+
createIfMissing: false,
|
|
409
|
+
});
|
|
410
|
+
await db.open();
|
|
411
|
+
try {
|
|
412
|
+
const records = [];
|
|
413
|
+
for await (const entry of db.iterator()) records.push(entry);
|
|
414
|
+
return checkSceneLevels(records, {
|
|
415
|
+
cliVersion: compendiumCliVersion(),
|
|
416
|
+
});
|
|
417
|
+
} finally {
|
|
418
|
+
await db.close();
|
|
419
|
+
}
|
|
420
|
+
}
|