@heroiclands/package-build 23.4.1 → 23.5.1
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 +35 -0
- package/README.md +29 -25
- package/assets/images/provenance.yaml +1 -0
- package/bin/package-build.mjs +259 -101
- package/ci/prettier.mjs +73 -0
- package/config.mjs +10 -0
- package/container.mjs +232 -1
- package/content-config.mjs +5 -1
- package/docs/api.md +100 -52
- package/docs/authoring/assets.md +16 -1
- package/docs/commands.md +154 -51
- package/docs/configuration.md +39 -2
- package/docs/content-format.md +30 -6
- package/docs/diagnostics.md +1 -2
- package/docs/guides/e2e.md +38 -0
- package/docs/guides/icons.md +5 -0
- package/docs/guides/module-project.md +1 -1
- package/docs/guides/release-and-deploy.md +36 -1
- package/docs/project-setup.md +16 -59
- package/docs/reference/format-details.md +180 -34
- package/docs/reference/note-types.md +69 -69
- package/engine/asset-index.mjs +63 -22
- package/engine/asset-references.mjs +355 -0
- package/engine/asset-shipping.mjs +363 -0
- package/engine/asset-types.mjs +70 -0
- package/engine/base-compiler.mjs +24 -1
- package/engine/cdn-publish.mjs +643 -0
- package/engine/index.mjs +6 -0
- package/engine/note-vocabulary.mjs +4 -1
- package/engine/pathnames.mjs +21 -10
- package/engine/project-init.mjs +4 -29
- package/engine/rank-ladder.mjs +40 -16
- package/engine/scenes.mjs +1 -1
- package/engine/standing-terms.mjs +3 -1
- package/package.json +5 -6
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +69 -7
- package/sohl/actors.mjs +12 -4
- package/sohl/default-item-art.mjs +23 -19
- package/types/config.d.mts +7 -0
- package/types/container.d.mts +137 -1
- package/types/engine/asset-index.d.mts +3 -2
- package/types/engine/asset-references.d.mts +136 -0
- package/types/engine/asset-shipping.d.mts +240 -0
- package/types/engine/asset-types.d.mts +36 -0
- package/types/engine/base-compiler.d.mts +7 -1
- package/types/engine/cdn-publish.d.mts +312 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/pathnames.d.mts +2 -1
- package/types/engine/rank-ladder.d.mts +25 -6
- package/types/release.d.mts +5 -3
- package/types/sohl/default-item-art.d.mts +2 -1
- package/labels.mjs +0 -191
- package/types/labels.d.mts +0 -40
|
@@ -0,0 +1,363 @@
|
|
|
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 **deployed layout** of a package's pictures, and which of them ship.
|
|
16
|
+
*
|
|
17
|
+
* A repository keeps `assets/icons/` and `assets/images/` arranged however its
|
|
18
|
+
* authors like. A Foundry package, the CDN and every location a GM fills hold
|
|
19
|
+
* them flat instead:
|
|
20
|
+
*
|
|
21
|
+
* ```text
|
|
22
|
+
* icons/<basename>
|
|
23
|
+
* images/<basename>
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* The root is the type and the basename is the shortcode with its extension,
|
|
27
|
+
* so the deployed path follows from the address alone. A root's shortcodes are
|
|
28
|
+
* one flat namespace however deeply its source nests — two files sharing one
|
|
29
|
+
* are already a finding in {@link module:engine/asset-index} — so flattening
|
|
30
|
+
* never collides. Audio is not part of this layout.
|
|
31
|
+
*
|
|
32
|
+
* **What ships.** The stage copies a picture into the package only when its
|
|
33
|
+
* resolved provenance record says `ai: false` and its `license` is on
|
|
34
|
+
* {@link SHIPPING_LICENSES}. Everything else stays in the repository. The
|
|
35
|
+
* selection is {@link selectStagedAssets}, one function, so the stage and any
|
|
36
|
+
* command that publishes what the stage leaves out read the same answer.
|
|
37
|
+
*
|
|
38
|
+
* **An asset set** is a JSON file listing the pictures one location holds:
|
|
39
|
+
*
|
|
40
|
+
* ```json
|
|
41
|
+
* {
|
|
42
|
+
* "name": "Thalorna — shipped art",
|
|
43
|
+
* "package": "thalorna",
|
|
44
|
+
* "assets": [
|
|
45
|
+
* { "address": "thalorna-none-image-thorn", "source": "../images/thorn.webp",
|
|
46
|
+
* "target": "images/thorn.webp", "size": 48213 }
|
|
47
|
+
* ]
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* `name` is the set's display name and `package` the content package its
|
|
52
|
+
* addresses belong to — the address's first segment, not the Foundry id. Each
|
|
53
|
+
* entry in `assets`:
|
|
54
|
+
*
|
|
55
|
+
* - `target` is the file's path relative to the location's root, in the
|
|
56
|
+
* deployed layout, extension included.
|
|
57
|
+
* - `source` is where the file can be fetched from, resolved against the URL of
|
|
58
|
+
* the set file holding it — `new URL(source, setUrl)`.
|
|
59
|
+
* - `size` is in bytes.
|
|
60
|
+
*
|
|
61
|
+
* The staged package's own set is always `asset-sets/default.json`, a fixed
|
|
62
|
+
* name, and names itself after the package's manifest title with
|
|
63
|
+
* ` — shipped art` appended. Its files sit one folder up from it, so each
|
|
64
|
+
* `source` is `../<target>`.
|
|
65
|
+
*
|
|
66
|
+
* @module
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
import fs from "node:fs";
|
|
70
|
+
import path from "node:path";
|
|
71
|
+
|
|
72
|
+
import { collectAssetRecords } from "./asset-index.mjs";
|
|
73
|
+
import { canonicalKey } from "./content-address.mjs";
|
|
74
|
+
import { ASSET_SYSTEM, DEPLOYED_ASSET_TYPES, deployedSuffix } from "./asset-types.mjs";
|
|
75
|
+
|
|
76
|
+
export { DEPLOYED_ASSET_TYPES, deployedSuffix } from "./asset-types.mjs";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The licences a shipped picture may carry, as its provenance `license` spells
|
|
80
|
+
* them.
|
|
81
|
+
*
|
|
82
|
+
* A package adds to this list with `packageBuild.shippingLicenses`; the list a
|
|
83
|
+
* package ships under is this one together with its additions.
|
|
84
|
+
*
|
|
85
|
+
* `LicenseRef-PublicDomain` is the spelling for a work in the public domain,
|
|
86
|
+
* which SPDX gives no identifier of its own. `CC0` is accepted beside
|
|
87
|
+
* `CC0-1.0` because both spellings name the one dedication. Nothing is
|
|
88
|
+
* normalised: a value off this list is excluded, however close it reads.
|
|
89
|
+
*
|
|
90
|
+
* @type {readonly string[]}
|
|
91
|
+
*/
|
|
92
|
+
export const SHIPPING_LICENSES = Object.freeze([
|
|
93
|
+
"LicenseRef-PublicDomain",
|
|
94
|
+
"CC0-1.0",
|
|
95
|
+
"CC0",
|
|
96
|
+
"CC-BY-3.0",
|
|
97
|
+
"CC-BY-4.0",
|
|
98
|
+
"CC-BY-SA-4.0",
|
|
99
|
+
"LicenseRef-NounProject-Pro",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The staged package's own asset set, relative to the package root — the set
|
|
104
|
+
* listing the pictures it ships.
|
|
105
|
+
*
|
|
106
|
+
* @type {string}
|
|
107
|
+
*/
|
|
108
|
+
export const SHIPPED_SET_FILE = "asset-sets/default.json";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The shortcode a package's stand-in picture carries under each deployed root.
|
|
112
|
+
*
|
|
113
|
+
* @type {string}
|
|
114
|
+
*/
|
|
115
|
+
export const STAND_IN_SHORTCODE = "missing";
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The deployed path of one asset record, or `undefined` for a record of a type
|
|
119
|
+
* that does not deploy flat.
|
|
120
|
+
*
|
|
121
|
+
* @param {{type?: string, asset?: {path?: string}}} record - An asset record.
|
|
122
|
+
* @returns {string|undefined} `<icons|images>/<basename>`.
|
|
123
|
+
*/
|
|
124
|
+
export function deployedAssetPath(record) {
|
|
125
|
+
const source = record?.asset?.path;
|
|
126
|
+
if (!source) return undefined;
|
|
127
|
+
const type = DEPLOYED_ASSET_TYPES.find((entry) => entry.type === record.type);
|
|
128
|
+
if (!type) return undefined;
|
|
129
|
+
const deployed = deployedSuffix(source);
|
|
130
|
+
return deployed.startsWith(`${type.root}/`) ? deployed : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Why one asset record does not ship, or `""` when it does.
|
|
135
|
+
*
|
|
136
|
+
* @param {{asset?: {ai?: unknown, license?: unknown}}} record - An asset record.
|
|
137
|
+
* @param {readonly string[]} [additions] - Licences the package adds to
|
|
138
|
+
* {@link SHIPPING_LICENSES}, from `packageBuild.shippingLicenses`.
|
|
139
|
+
* @returns {string} The reason, as a clause, or `""`.
|
|
140
|
+
*/
|
|
141
|
+
export function exclusionReason(record, additions = []) {
|
|
142
|
+
const ai = record?.asset?.ai;
|
|
143
|
+
if (ai === true) return "it is machine-generated (`ai: true`)";
|
|
144
|
+
if (ai !== false) return "its provenance does not state `ai`";
|
|
145
|
+
const license = record?.asset?.license;
|
|
146
|
+
if (
|
|
147
|
+
!SHIPPING_LICENSES.includes(/** @type {string} */ (license)) &&
|
|
148
|
+
!additions.includes(/** @type {string} */ (license))
|
|
149
|
+
) {
|
|
150
|
+
return license ?
|
|
151
|
+
`its licence \`${license}\` is not one a package ships`
|
|
152
|
+
: "its provenance states no licence";
|
|
153
|
+
}
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Whether one asset record ships in the Foundry package.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} record - An asset record.
|
|
161
|
+
* @param {readonly string[]} [additions] - The package's added licences.
|
|
162
|
+
* @returns {boolean} True for a deployed type with `ai: false` and a licence on
|
|
163
|
+
* {@link SHIPPING_LICENSES} or in `additions`.
|
|
164
|
+
*/
|
|
165
|
+
export function shipsAsset(record, additions = []) {
|
|
166
|
+
return deployedAssetPath(record) !== undefined && exclusionReason(record, additions) === "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The canonical address of a package's stand-in for one asset type.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} pkg - The content package.
|
|
173
|
+
* @param {string} type - `icon` or `image`.
|
|
174
|
+
* @returns {string} `<package>-none-<type>-missing`.
|
|
175
|
+
*/
|
|
176
|
+
export function standInAddress(pkg, type) {
|
|
177
|
+
return canonicalKey(pkg, ASSET_SYSTEM, type, STAND_IN_SHORTCODE);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Split a package's picture records into those the stage ships and those it
|
|
182
|
+
* leaves out.
|
|
183
|
+
*
|
|
184
|
+
* Audio and every other record are in neither list.
|
|
185
|
+
*
|
|
186
|
+
* @param {readonly object[]} records - Asset records, in any order.
|
|
187
|
+
* @param {readonly string[]} [additions] - The package's added licences.
|
|
188
|
+
* @returns {{staged: object[], excluded: {record: object, reason: string}[]}}
|
|
189
|
+
* Both in address order.
|
|
190
|
+
*/
|
|
191
|
+
export function selectStagedAssets(records, additions = []) {
|
|
192
|
+
const pictures = records
|
|
193
|
+
.filter((record) => deployedAssetPath(record) !== undefined)
|
|
194
|
+
.sort((a, b) => (a.address.canonical < b.address.canonical ? -1 : 1));
|
|
195
|
+
const staged = [];
|
|
196
|
+
const excluded = [];
|
|
197
|
+
for (const record of pictures) {
|
|
198
|
+
const reason = exclusionReason(record, additions);
|
|
199
|
+
if (reason) excluded.push({ record, reason });
|
|
200
|
+
else staged.push(record);
|
|
201
|
+
}
|
|
202
|
+
return { staged, excluded };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* One asset-set entry.
|
|
207
|
+
*
|
|
208
|
+
* @typedef {object} AssetsManifestEntry
|
|
209
|
+
* @property {string} address - The canonical address.
|
|
210
|
+
* @property {string} source - Where the file is fetched from, resolved against
|
|
211
|
+
* the URL of the set file holding it.
|
|
212
|
+
* @property {string} target - Its path relative to the location's root, in the
|
|
213
|
+
* deployed layout.
|
|
214
|
+
* @property {number} size - Its size in bytes.
|
|
215
|
+
*/
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* An asset set: a display name, the package its addresses belong to, and one
|
|
219
|
+
* entry per file.
|
|
220
|
+
*
|
|
221
|
+
* @typedef {object} AssetSet
|
|
222
|
+
* @property {string} name - The set's display name.
|
|
223
|
+
* @property {string} package - The content package.
|
|
224
|
+
* @property {AssetsManifestEntry[]} assets - The files, in target order.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The display name of a staged package's own asset set.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} title - The package's manifest title.
|
|
231
|
+
* @returns {string} `<title> — shipped art`.
|
|
232
|
+
*/
|
|
233
|
+
export function shippedSetName(title) {
|
|
234
|
+
return `${title} — shipped art`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The asset-set entries for a set of staged files, as the staged package's own
|
|
239
|
+
* set lists them.
|
|
240
|
+
*
|
|
241
|
+
* The set sits in `asset-sets/` under the package root, so each `source` is
|
|
242
|
+
* `../<target>`.
|
|
243
|
+
*
|
|
244
|
+
* @param {readonly object[]} staged - The records staged.
|
|
245
|
+
* @param {string} stageDir - The staged package root, where each file's size
|
|
246
|
+
* is measured — after any transform, since that is the file a location holds.
|
|
247
|
+
* @returns {AssetsManifestEntry[]} In target order.
|
|
248
|
+
*/
|
|
249
|
+
export function assetsManifest(staged, stageDir) {
|
|
250
|
+
return staged
|
|
251
|
+
.map((record) => {
|
|
252
|
+
const target = /** @type {string} */ (deployedAssetPath(record));
|
|
253
|
+
return {
|
|
254
|
+
address: record.address.canonical,
|
|
255
|
+
source: path.posix.relative(path.posix.dirname(SHIPPED_SET_FILE), target),
|
|
256
|
+
target,
|
|
257
|
+
size: fs.statSync(path.join(stageDir, ...target.split("/"))).size,
|
|
258
|
+
};
|
|
259
|
+
})
|
|
260
|
+
.sort((a, b) => (a.target < b.target ? -1 : 1));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Stage a package's shipped pictures flat at the package root, and write the
|
|
265
|
+
* package's own asset set, `asset-sets/default.json`.
|
|
266
|
+
*
|
|
267
|
+
* The records are read by {@link module:engine/asset-index.collectAssetRecords},
|
|
268
|
+
* so provenance resolves exactly as the content index resolves it — per file,
|
|
269
|
+
* a sidecar replacing an inherited record. Any finding that walk makes stops the
|
|
270
|
+
* stage before a file is copied: a package whose provenance cannot be read is
|
|
271
|
+
* not one whose shipping can be decided.
|
|
272
|
+
*
|
|
273
|
+
* @param {object} opts - Options.
|
|
274
|
+
* @param {string} opts.assetsBase - The package's asset directory, the roots'
|
|
275
|
+
* parent.
|
|
276
|
+
* @param {string} opts.contentPackage - The package the trees belong to.
|
|
277
|
+
* @param {string} opts.stageDir - The staged package root.
|
|
278
|
+
* @param {string} opts.title - The package's manifest title, which names the
|
|
279
|
+
* set.
|
|
280
|
+
* @param {(sourcePath: string) => string|null|undefined} [opts.transform] -
|
|
281
|
+
* The repository's per-file transform.
|
|
282
|
+
* @param {readonly string[]} [opts.shippingLicenses] - Licences the package
|
|
283
|
+
* adds to {@link SHIPPING_LICENSES}.
|
|
284
|
+
* @returns {{staged: object[], excluded: {record: object, reason: string}[],
|
|
285
|
+
* findings: object[], manifest: AssetSet|null}}
|
|
286
|
+
* What was staged and left out, and the findings that stopped the stage —
|
|
287
|
+
* nothing is staged when `findings` is non-empty. A package with neither
|
|
288
|
+
* picture root stages nothing and writes no asset set.
|
|
289
|
+
*/
|
|
290
|
+
export function stagePackageAssets({
|
|
291
|
+
assetsBase,
|
|
292
|
+
contentPackage,
|
|
293
|
+
stageDir,
|
|
294
|
+
title,
|
|
295
|
+
transform,
|
|
296
|
+
shippingLicenses = [],
|
|
297
|
+
}) {
|
|
298
|
+
const findings = [];
|
|
299
|
+
const hasPictures = DEPLOYED_ASSET_TYPES.some(({ root }) =>
|
|
300
|
+
fs.existsSync(path.join(assetsBase, root)),
|
|
301
|
+
);
|
|
302
|
+
if (!hasPictures) return { staged: [], excluded: [], findings, manifest: null };
|
|
303
|
+
const records = collectAssetRecords(assetsBase, { contentPackage, problems: findings });
|
|
304
|
+
if (findings.length) return { staged: [], excluded: [], findings, manifest: null };
|
|
305
|
+
|
|
306
|
+
const { staged, excluded } = selectStagedAssets(records, shippingLicenses);
|
|
307
|
+
for (const { root } of DEPLOYED_ASSET_TYPES) {
|
|
308
|
+
fs.rmSync(path.join(stageDir, root), { recursive: true, force: true });
|
|
309
|
+
}
|
|
310
|
+
for (const record of staged) {
|
|
311
|
+
const from = path.join(assetsBase, ...record.asset.path.split("/"));
|
|
312
|
+
const to = path.join(
|
|
313
|
+
stageDir,
|
|
314
|
+
.../** @type {string} */ (deployedAssetPath(record)).split("/"),
|
|
315
|
+
);
|
|
316
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
317
|
+
const transformed = transform ? transform(from) : null;
|
|
318
|
+
if (transformed != null) fs.writeFileSync(to, transformed);
|
|
319
|
+
else fs.copyFileSync(from, to);
|
|
320
|
+
}
|
|
321
|
+
const manifest = {
|
|
322
|
+
name: shippedSetName(title),
|
|
323
|
+
package: contentPackage,
|
|
324
|
+
assets: assetsManifest(staged, stageDir),
|
|
325
|
+
};
|
|
326
|
+
const setFile = path.join(stageDir, ...SHIPPED_SET_FILE.split("/"));
|
|
327
|
+
fs.mkdirSync(path.dirname(setFile), { recursive: true });
|
|
328
|
+
fs.writeFileSync(setFile, `${JSON.stringify(manifest, null, 4)}\n`);
|
|
329
|
+
return { staged, excluded, findings, manifest };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The `packageBuild.assets` rows that would stage a picture root themselves.
|
|
334
|
+
*
|
|
335
|
+
* The stage copies `icons/` and `images/` itself, filtered; a row copying
|
|
336
|
+
* either root, a directory inside one, or a directory holding one would put
|
|
337
|
+
* every file back unfiltered, so each is refused.
|
|
338
|
+
*
|
|
339
|
+
* @param {readonly {from: string, to: string}[]} rows - The configured rows.
|
|
340
|
+
* @param {object} opts - Options.
|
|
341
|
+
* @param {string} opts.rootDir - The repository root rows resolve against.
|
|
342
|
+
* @param {string} opts.assetsBase - The package's asset directory.
|
|
343
|
+
* @returns {{index: number, from: string, root: string}[]} One per offending
|
|
344
|
+
* row, naming the root it reaches.
|
|
345
|
+
*/
|
|
346
|
+
export function rowsStagingPictureRoots(rows, { rootDir, assetsBase }) {
|
|
347
|
+
const out = [];
|
|
348
|
+
rows.forEach(({ from }, index) => {
|
|
349
|
+
const source = path.resolve(rootDir, from);
|
|
350
|
+
for (const { root } of DEPLOYED_ASSET_TYPES) {
|
|
351
|
+
const rootPath = path.resolve(assetsBase, root);
|
|
352
|
+
const inside = (child, parent) => {
|
|
353
|
+
const rel = path.relative(parent, child);
|
|
354
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
355
|
+
};
|
|
356
|
+
if (inside(source, rootPath) || inside(rootPath, source)) {
|
|
357
|
+
out.push({ index, from, root: path.relative(rootDir, rootPath) });
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
return out;
|
|
363
|
+
}
|
package/engine/asset-types.mjs
CHANGED
|
@@ -148,6 +148,76 @@ export const ASSET_TYPES = Object.freeze([
|
|
|
148
148
|
}),
|
|
149
149
|
]);
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The asset types that deploy flat — `icon` and `image`, never `audio`.
|
|
153
|
+
*
|
|
154
|
+
* @type {readonly import("./asset-types.mjs").AssetType[]}
|
|
155
|
+
*/
|
|
156
|
+
export const DEPLOYED_ASSET_TYPES = Object.freeze(
|
|
157
|
+
ASSET_TYPES.filter((entry) => entry.type === "icon" || entry.type === "image"),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The deployed roots, by name.
|
|
162
|
+
*
|
|
163
|
+
* @type {ReadonlySet<string>}
|
|
164
|
+
*/
|
|
165
|
+
const DEPLOYED_ROOTS = new Set(DEPLOYED_ASSET_TYPES.map((entry) => entry.root));
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether a source suffix sits under a picture root, and so deploys flat.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} suffix - A path inside a package's `assets/` directory.
|
|
171
|
+
* @returns {boolean} True for `icons/…` and `images/…`.
|
|
172
|
+
*/
|
|
173
|
+
export function isDeployedSuffix(suffix) {
|
|
174
|
+
const segments = String(suffix).split("/");
|
|
175
|
+
return segments.length >= 2 && DEPLOYED_ROOTS.has(segments[0]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A source suffix rewritten into the deployed layout.
|
|
180
|
+
*
|
|
181
|
+
* `images/beings/characters/thorn.webp` becomes `images/thorn.webp`. A suffix
|
|
182
|
+
* under no deployed root — `audio/…`, `ui/…`, `generated/…` — is returned
|
|
183
|
+
* unchanged, because only the picture roots deploy flat.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} suffix - A path inside a package's `assets/` directory,
|
|
186
|
+
* `/`-separated.
|
|
187
|
+
* @returns {string} The path the file has in a deployed location.
|
|
188
|
+
*/
|
|
189
|
+
export function deployedSuffix(suffix) {
|
|
190
|
+
if (!isDeployedSuffix(suffix)) return suffix;
|
|
191
|
+
const segments = String(suffix).split("/");
|
|
192
|
+
return `${segments[0]}/${segments[segments.length - 1]}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* What is wrong with a package's `packageBuild.shippingLicenses`, or `""` when
|
|
197
|
+
* nothing is.
|
|
198
|
+
*
|
|
199
|
+
* The key lists licences a package adds to the default shipping list, spelled
|
|
200
|
+
* as provenance spells them: a list of non-empty strings with no duplicates.
|
|
201
|
+
*
|
|
202
|
+
* @param {unknown} value - The declared value; `undefined` is absent and fine.
|
|
203
|
+
* @returns {string} The problem as a clause, or `""`.
|
|
204
|
+
*/
|
|
205
|
+
export function shippingLicensesProblem(value) {
|
|
206
|
+
if (value === undefined) return "";
|
|
207
|
+
if (!Array.isArray(value)) return "must be a list of licence identifiers";
|
|
208
|
+
for (const [index, entry] of value.entries()) {
|
|
209
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
210
|
+
return `entry ${index} must be a non-empty string`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
for (const entry of value) {
|
|
215
|
+
if (seen.has(entry)) return `lists \`${entry}\` more than once`;
|
|
216
|
+
seen.add(entry);
|
|
217
|
+
}
|
|
218
|
+
return "";
|
|
219
|
+
}
|
|
220
|
+
|
|
151
221
|
/**
|
|
152
222
|
* Every asset type name.
|
|
153
223
|
*
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -108,6 +108,8 @@ import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.
|
|
|
108
108
|
// `engine/being-age.mjs`.
|
|
109
109
|
import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
|
|
110
110
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
111
|
+
import { loadPackConfig } from "./pack-config.mjs";
|
|
112
|
+
import { annotateAssetReferences, assetReferenceLookup } from "./asset-references.mjs";
|
|
111
113
|
|
|
112
114
|
/**
|
|
113
115
|
* The tallies one pass accumulates while walking the tree.
|
|
@@ -449,6 +451,9 @@ export class BasePackCompiler {
|
|
|
449
451
|
/** @type {object|undefined} */
|
|
450
452
|
#stats;
|
|
451
453
|
|
|
454
|
+
/** Every picture a document can name, built once per pass. */
|
|
455
|
+
#assetLookup;
|
|
456
|
+
|
|
452
457
|
/**
|
|
453
458
|
* Whether this pass's pack is the one a claimed note belongs in.
|
|
454
459
|
*
|
|
@@ -1089,10 +1094,28 @@ export class BasePackCompiler {
|
|
|
1089
1094
|
/**
|
|
1090
1095
|
* Write one document into a directory, named for its name and id.
|
|
1091
1096
|
*
|
|
1097
|
+
* Every asset the document references states its address on the way out,
|
|
1098
|
+
* and an asset its package does not ship is drawn by that package's
|
|
1099
|
+
* stand-in — see {@link module:engine/asset-references}.
|
|
1100
|
+
*
|
|
1092
1101
|
* @param {string} dir - The destination directory.
|
|
1093
1102
|
* @param {object} doc - The document.
|
|
1103
|
+
* @param {string} [documentType] - Its Foundry document type; this pass's
|
|
1104
|
+
* own by default.
|
|
1094
1105
|
*/
|
|
1095
|
-
writeTo(dir, doc) {
|
|
1106
|
+
writeTo(dir, doc, documentType = this.docType) {
|
|
1107
|
+
this.#assetLookup ??= assetReferenceLookup(this.linkIndex, loadPackConfig());
|
|
1108
|
+
const reported = new Set();
|
|
1109
|
+
annotateAssetReferences(doc, documentType, {
|
|
1110
|
+
lookup: this.#assetLookup,
|
|
1111
|
+
system: doc?._stats?.systemId ?? this.stats?.systemId,
|
|
1112
|
+
report: (message) => {
|
|
1113
|
+
if (reported.has(message)) return;
|
|
1114
|
+
reported.add(message);
|
|
1115
|
+
this.errorCount++;
|
|
1116
|
+
this.noteError(message);
|
|
1117
|
+
},
|
|
1118
|
+
});
|
|
1096
1119
|
fs.writeFileSync(
|
|
1097
1120
|
path.join(dir, makeFilename(doc.name, doc._id)),
|
|
1098
1121
|
JSON.stringify(doc, null, 2),
|