@heroiclands/package-build 23.4.0 → 23.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +29 -25
- package/assets/images/provenance.yaml +1 -0
- package/bin/package-build.mjs +259 -28
- package/ci/prettier.mjs +73 -0
- package/config.mjs +10 -0
- package/container.mjs +232 -1
- package/content-config.mjs +5 -1
- package/docs/api.md +100 -52
- package/docs/authoring/assets.md +16 -1
- package/docs/commands.md +154 -7
- package/docs/configuration.md +39 -2
- package/docs/content-format.md +30 -6
- package/docs/guides/e2e.md +38 -0
- package/docs/guides/icons.md +5 -0
- package/docs/guides/release-and-deploy.md +36 -1
- package/docs/reference/format-details.md +218 -50
- package/docs/reference/note-types.md +71 -71
- package/engine/asset-index.mjs +63 -22
- package/engine/asset-references.mjs +355 -0
- package/engine/asset-shipping.mjs +363 -0
- package/engine/asset-types.mjs +70 -0
- package/engine/base-compiler.mjs +24 -1
- package/engine/cdn-publish.mjs +618 -0
- package/engine/index.mjs +6 -0
- package/engine/note-vocabulary.mjs +40 -2
- package/engine/pathnames.mjs +21 -10
- package/engine/rank-ladder.mjs +40 -16
- package/engine/scenes.mjs +1 -1
- package/engine/standing-terms.mjs +3 -1
- package/package.json +4 -3
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +69 -7
- package/sohl/actors.mjs +12 -4
- package/sohl/default-item-art.mjs +23 -19
- package/types/config.d.mts +7 -0
- package/types/container.d.mts +137 -1
- package/types/engine/asset-index.d.mts +3 -2
- package/types/engine/asset-references.d.mts +136 -0
- package/types/engine/asset-shipping.d.mts +240 -0
- package/types/engine/asset-types.d.mts +36 -0
- package/types/engine/base-compiler.d.mts +7 -1
- package/types/engine/cdn-publish.d.mts +311 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/pathnames.d.mts +2 -1
- package/types/engine/rank-ladder.d.mts +25 -6
- package/types/release.d.mts +5 -3
- package/types/sohl/default-item-art.d.mts +2 -1
package/container.mjs
CHANGED
|
@@ -380,6 +380,216 @@ export function needsFoundryAccount({
|
|
|
380
380
|
return !licensed;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/** The S3 image: MinIO images cannot be pulled everywhere, SeaweedFS can. */
|
|
384
|
+
export const S3_IMAGE = "chrislusf/seaweedfs:3.80";
|
|
385
|
+
|
|
386
|
+
/** The name the S3 server answers to on the shared network, and in `awsConfig`. */
|
|
387
|
+
export const S3_ALIAS = "s3.localhost";
|
|
388
|
+
|
|
389
|
+
/** The port the S3 server listens on and publishes. */
|
|
390
|
+
export const S3_PORT = 8333;
|
|
391
|
+
|
|
392
|
+
/** The bucket the S3 server is created with. */
|
|
393
|
+
export const S3_BUCKET = "sohl-e2e";
|
|
394
|
+
|
|
395
|
+
/** The consumer's S3 identities, relative to the repository root. */
|
|
396
|
+
export const S3_IDENTITIES_FILE = "cypress/s3/s3-identities.json";
|
|
397
|
+
|
|
398
|
+
/** The consumer's Foundry `awsConfig`, relative to the repository root. */
|
|
399
|
+
export const S3_AWS_CONFIG_FILE = "cypress/s3/aws-e2e.json";
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Whether the S3 server is asked for: `FOUNDRYVTT_CONTAINER_S3` set to `1` or
|
|
403
|
+
* `true`. Anything else, including unset, leaves the container as it is.
|
|
404
|
+
*
|
|
405
|
+
* @param {NodeJS.ProcessEnv} [env] - Environment to read.
|
|
406
|
+
* @returns {boolean} Whether to run the S3 server.
|
|
407
|
+
*/
|
|
408
|
+
export function s3Requested(env = process.env) {
|
|
409
|
+
return ["1", "true"].includes(env.FOUNDRYVTT_CONTAINER_S3?.trim().toLowerCase() ?? "");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Everything the S3 opt-in does, as data.
|
|
414
|
+
*
|
|
415
|
+
* @typedef {object} S3Plan
|
|
416
|
+
* @property {string} network - The user-defined network the Foundry container
|
|
417
|
+
* and the S3 server share.
|
|
418
|
+
* @property {string} server - The S3 server's container name.
|
|
419
|
+
* @property {string} identities - Absolute path of the identities file.
|
|
420
|
+
* @property {string} awsConfig - Absolute path of the Foundry `awsConfig`.
|
|
421
|
+
* @property {string} awsConfigName - The file's name under the data root's
|
|
422
|
+
* `Config/`, and the value of `FOUNDRY_AWS_CONFIG`.
|
|
423
|
+
*/
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The S3 plan for a container, or `null` when the opt-in is off.
|
|
427
|
+
*
|
|
428
|
+
* @param {object} opts
|
|
429
|
+
* @param {string} opts.name - The Foundry container's name.
|
|
430
|
+
* @param {string} opts.rootDir - The consumer repository root, which holds the
|
|
431
|
+
* identities and the `awsConfig`.
|
|
432
|
+
* @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
|
|
433
|
+
* @returns {S3Plan|null} The plan.
|
|
434
|
+
*/
|
|
435
|
+
export function resolveS3({ name, rootDir, env = process.env }) {
|
|
436
|
+
if (!s3Requested(env)) return null;
|
|
437
|
+
return {
|
|
438
|
+
network: `${name}-net`,
|
|
439
|
+
server: `${name}-s3`,
|
|
440
|
+
identities: path.resolve(rootDir, S3_IDENTITIES_FILE),
|
|
441
|
+
awsConfig: path.resolve(rootDir, S3_AWS_CONFIG_FILE),
|
|
442
|
+
awsConfigName: path.basename(S3_AWS_CONFIG_FILE),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The `docker run` argument vector for the S3 server.
|
|
448
|
+
*
|
|
449
|
+
* @param {S3Plan} plan - The S3 plan.
|
|
450
|
+
* @returns {string[]} Arguments after `docker`.
|
|
451
|
+
*/
|
|
452
|
+
export function s3RunArgs(plan) {
|
|
453
|
+
return [
|
|
454
|
+
"run",
|
|
455
|
+
"--detach",
|
|
456
|
+
"--name",
|
|
457
|
+
plan.server,
|
|
458
|
+
"--network",
|
|
459
|
+
plan.network,
|
|
460
|
+
"--network-alias",
|
|
461
|
+
S3_ALIAS,
|
|
462
|
+
"--publish",
|
|
463
|
+
`${S3_PORT}:${S3_PORT}`,
|
|
464
|
+
"--volume",
|
|
465
|
+
`${plan.identities}:/etc/seaweedfs/s3.json:ro`,
|
|
466
|
+
S3_IMAGE,
|
|
467
|
+
"server",
|
|
468
|
+
"-dir=/data",
|
|
469
|
+
"-s3",
|
|
470
|
+
`-s3.port=${S3_PORT}`,
|
|
471
|
+
"-s3.config=/etc/seaweedfs/s3.json",
|
|
472
|
+
"-s3.allowedOrigins=*",
|
|
473
|
+
];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The `docker exec` argument vector that runs one `weed shell` command in the
|
|
478
|
+
* S3 server.
|
|
479
|
+
*
|
|
480
|
+
* @param {S3Plan} plan - The S3 plan.
|
|
481
|
+
* @param {string} command - The shell command, such as `s3.bucket.list`.
|
|
482
|
+
* @returns {string[]} Arguments after `docker`.
|
|
483
|
+
*/
|
|
484
|
+
export function s3ShellArgs(plan, command) {
|
|
485
|
+
return [
|
|
486
|
+
"exec",
|
|
487
|
+
plan.server,
|
|
488
|
+
"sh",
|
|
489
|
+
"-c",
|
|
490
|
+
`echo "${command}" | weed shell -master=localhost:9333`,
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Start the S3 server on its network and create the bucket, unless it is
|
|
496
|
+
* already running, and copy the `awsConfig` into the data root.
|
|
497
|
+
*
|
|
498
|
+
* @param {object} opts
|
|
499
|
+
* @param {S3Plan} opts.plan - The S3 plan.
|
|
500
|
+
* @param {string} opts.dataRoot - The Foundry user-data root.
|
|
501
|
+
* @param {(message: string) => void} [opts.log] - Progress reporting.
|
|
502
|
+
* @param {(args: string[]) => number} [opts.run] - Runs docker, returning the
|
|
503
|
+
* exit status.
|
|
504
|
+
* @param {(args: string[]) => string} [opts.capture] - Runs docker, returning
|
|
505
|
+
* its stdout, or `""` on failure.
|
|
506
|
+
* @param {(ms: number) => void} [opts.sleep] - Waits.
|
|
507
|
+
* @param {number} [opts.attempts] - How often to ask the server whether it is
|
|
508
|
+
* ready.
|
|
509
|
+
* @throws {Error} When an input file is missing or the server never answers.
|
|
510
|
+
*/
|
|
511
|
+
export function ensureS3({
|
|
512
|
+
plan,
|
|
513
|
+
dataRoot,
|
|
514
|
+
log = () => {},
|
|
515
|
+
run = runDocker,
|
|
516
|
+
capture = captureDocker,
|
|
517
|
+
sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms),
|
|
518
|
+
attempts = 60,
|
|
519
|
+
}) {
|
|
520
|
+
for (const file of [plan.identities, plan.awsConfig]) {
|
|
521
|
+
if (!fs.existsSync(file)) {
|
|
522
|
+
throw new Error(`FOUNDRYVTT_CONTAINER_S3 needs ${file}, which does not exist.`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const configDir = path.join(dataRoot, "Config");
|
|
526
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
527
|
+
fs.copyFileSync(plan.awsConfig, path.join(configDir, plan.awsConfigName));
|
|
528
|
+
|
|
529
|
+
if (
|
|
530
|
+
!capture(["network", "ls", "--filter", `name=^${plan.network}$`, "--format", "{{.Name}}"])
|
|
531
|
+
) {
|
|
532
|
+
run(["network", "create", plan.network]);
|
|
533
|
+
}
|
|
534
|
+
const present = capture([
|
|
535
|
+
"ps",
|
|
536
|
+
"-a",
|
|
537
|
+
"--filter",
|
|
538
|
+
`name=^${plan.server}$`,
|
|
539
|
+
"--format",
|
|
540
|
+
"{{.Names}}",
|
|
541
|
+
]);
|
|
542
|
+
if (present.split("\n").includes(plan.server)) {
|
|
543
|
+
const running = capture([
|
|
544
|
+
"ps",
|
|
545
|
+
"--filter",
|
|
546
|
+
`name=^${plan.server}$`,
|
|
547
|
+
"--format",
|
|
548
|
+
"{{.Names}}",
|
|
549
|
+
]);
|
|
550
|
+
if (!running.split("\n").includes(plan.server)) run(["start", plan.server]);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
log(`Starting S3 server '${plan.server}' (${S3_IMAGE}) as ${S3_ALIAS}:${S3_PORT}`);
|
|
554
|
+
const status = run(s3RunArgs(plan));
|
|
555
|
+
if (status !== 0) throw new Error(`Starting the S3 server failed with status ${status}.`);
|
|
556
|
+
// `weed shell` exits 0 whether or not the filer is up; it names the filers
|
|
557
|
+
// it found once there is one.
|
|
558
|
+
for (let attempt = 0; ; attempt++) {
|
|
559
|
+
if (capture(s3ShellArgs(plan, "s3.bucket.list")).includes("filers: [")) break;
|
|
560
|
+
if (attempt >= attempts) {
|
|
561
|
+
throw new Error(`The S3 server '${plan.server}' did not become ready.`);
|
|
562
|
+
}
|
|
563
|
+
sleep(1000);
|
|
564
|
+
}
|
|
565
|
+
run(s3ShellArgs(plan, `s3.bucket.create -name ${S3_BUCKET}`));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Remove the S3 server and its network, tolerating either being absent.
|
|
570
|
+
*
|
|
571
|
+
* @param {S3Plan} plan - The S3 plan.
|
|
572
|
+
* @param {(args: string[]) => number} [run] - Runs docker.
|
|
573
|
+
* @param {(args: string[]) => string} [capture] - Runs docker for its stdout.
|
|
574
|
+
*/
|
|
575
|
+
export function removeS3(plan, run = runDocker, capture = captureDocker) {
|
|
576
|
+
const present = capture([
|
|
577
|
+
"ps",
|
|
578
|
+
"-a",
|
|
579
|
+
"--filter",
|
|
580
|
+
`name=^${plan.server}$`,
|
|
581
|
+
"--format",
|
|
582
|
+
"{{.Names}}",
|
|
583
|
+
]);
|
|
584
|
+
if (present.split("\n").includes(plan.server)) {
|
|
585
|
+
run(["stop", plan.server]);
|
|
586
|
+
run(["rm", plan.server]);
|
|
587
|
+
}
|
|
588
|
+
if (capture(["network", "ls", "--filter", `name=^${plan.network}$`, "--format", "{{.Name}}"])) {
|
|
589
|
+
run(["network", "rm", plan.network]);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
383
593
|
/**
|
|
384
594
|
* The full `docker run` argument vector for a stage's container.
|
|
385
595
|
*
|
|
@@ -399,6 +609,11 @@ export function needsFoundryAccount({
|
|
|
399
609
|
* @param {string|null} [opts.licenseKey] - Dedicated licence key.
|
|
400
610
|
* @param {boolean} [opts.account] - Whether to pass `FOUNDRY_USERNAME` and
|
|
401
611
|
* `FOUNDRY_PASSWORD` through; see {@link needsFoundryAccount}.
|
|
612
|
+
* @param {string|null} [opts.network] - A user-defined network to attach the
|
|
613
|
+
* container to as it is created, so it can reach the S3 server before Foundry
|
|
614
|
+
* starts.
|
|
615
|
+
* @param {string|null} [opts.awsConfig] - Name of the `awsConfig` file under the
|
|
616
|
+
* data root's `Config/`, passed as `FOUNDRY_AWS_CONFIG`.
|
|
402
617
|
* @param {string|null} [opts.user] - `--user` value to run the container as.
|
|
403
618
|
* `felddy/foundryvtt` runs Foundry as user 1000 and ignores `FOUNDRY_UID` and
|
|
404
619
|
* `FOUNDRY_GID`; under rootless Docker, container root is the host user, so
|
|
@@ -417,6 +632,8 @@ export function dockerRunArgs({
|
|
|
417
632
|
world = null,
|
|
418
633
|
licenseKey = null,
|
|
419
634
|
account = true,
|
|
635
|
+
network = null,
|
|
636
|
+
awsConfig = null,
|
|
420
637
|
user = null,
|
|
421
638
|
}) {
|
|
422
639
|
const args = [
|
|
@@ -444,6 +661,8 @@ export function dockerRunArgs({
|
|
|
444
661
|
if (version) args.push("-e", `FOUNDRY_VERSION=${version}`);
|
|
445
662
|
if (world !== null) args.push("-e", `FOUNDRY_WORLD=${world}`);
|
|
446
663
|
if (licenseKey) args.push("-e", `FOUNDRY_LICENSE_KEY=${licenseKey}`);
|
|
664
|
+
if (network) args.push("--network", network);
|
|
665
|
+
if (awsConfig) args.push("-e", `FOUNDRY_AWS_CONFIG=${awsConfig}`);
|
|
447
666
|
if (user?.trim()) args.push("--user", user.trim());
|
|
448
667
|
args.push(image);
|
|
449
668
|
return args;
|
|
@@ -660,6 +879,7 @@ export function clearStaleLock(dataRoot, log = () => {}) {
|
|
|
660
879
|
* @property {string|null} licenseKey
|
|
661
880
|
* @property {string|null} cacheDir
|
|
662
881
|
* @property {string|null} user - From `FOUNDRYVTT_CONTAINER_USER`.
|
|
882
|
+
* @property {S3Plan|null} s3 - Set by `FOUNDRYVTT_CONTAINER_S3`.
|
|
663
883
|
*/
|
|
664
884
|
|
|
665
885
|
/**
|
|
@@ -680,9 +900,10 @@ export function resolveContainer({ stage, config, env = process.env }) {
|
|
|
680
900
|
e2eStage: config.e2eStage,
|
|
681
901
|
});
|
|
682
902
|
const port = resolveStagePort(stage, { env, stages });
|
|
903
|
+
const name = containerName(config.packageId, stage, config.containerName);
|
|
683
904
|
return {
|
|
684
905
|
stage,
|
|
685
|
-
name
|
|
906
|
+
name,
|
|
686
907
|
port,
|
|
687
908
|
image: resolveImage({
|
|
688
909
|
env,
|
|
@@ -696,6 +917,7 @@ export function resolveContainer({ stage, config, env = process.env }) {
|
|
|
696
917
|
licenseKey: resolveLicenseKey(stage, env),
|
|
697
918
|
cacheDir: env.FOUNDRYVTT_CACHE?.trim() || null,
|
|
698
919
|
user: env.FOUNDRYVTT_CONTAINER_USER?.trim() || null,
|
|
920
|
+
s3: resolveS3({ name, rootDir: config.rootDir, env }),
|
|
699
921
|
};
|
|
700
922
|
}
|
|
701
923
|
|
|
@@ -710,10 +932,16 @@ export function resolveContainer({ stage, config, env = process.env }) {
|
|
|
710
932
|
* @returns {number} The exit status.
|
|
711
933
|
*/
|
|
712
934
|
export function startContainer(container, { dataRoot, env = process.env, log = () => {} }) {
|
|
935
|
+
// Before Foundry starts, so its S3 client finds the server on the first try.
|
|
936
|
+
if (container.s3) ensureS3({ plan: container.s3, dataRoot, log });
|
|
713
937
|
if (containerExists(container.name)) {
|
|
714
938
|
// Nothing is running against this data root — we are about to start it
|
|
715
939
|
// — so any lock present was left by a previous crash.
|
|
716
940
|
if (!containerExists(container.name, true)) clearStaleLock(dataRoot, log);
|
|
941
|
+
if (container.s3) {
|
|
942
|
+
// Fails harmlessly when the container is already on the network.
|
|
943
|
+
captureDocker(["network", "connect", container.s3.network, container.name]);
|
|
944
|
+
}
|
|
717
945
|
log(`Starting existing container '${container.name}' → ${container.url}`);
|
|
718
946
|
return runDocker(["start", container.name]);
|
|
719
947
|
}
|
|
@@ -743,6 +971,8 @@ export function startContainer(container, { dataRoot, env = process.env, log = (
|
|
|
743
971
|
version: container.version,
|
|
744
972
|
licenseKey: container.licenseKey ?? env.FOUNDRY_LICENSE_KEY?.trim() ?? null,
|
|
745
973
|
}),
|
|
974
|
+
network: container.s3?.network ?? null,
|
|
975
|
+
awsConfig: container.s3?.awsConfigName ?? null,
|
|
746
976
|
user: container.user,
|
|
747
977
|
}),
|
|
748
978
|
);
|
|
@@ -818,6 +1048,7 @@ export function containerAction({ action, stage, config, env = process.env, log
|
|
|
818
1048
|
}
|
|
819
1049
|
case "rm":
|
|
820
1050
|
removeContainer(container.name, log);
|
|
1051
|
+
if (container.s3) removeS3(container.s3);
|
|
821
1052
|
return 0;
|
|
822
1053
|
case "status":
|
|
823
1054
|
return runDocker(["ps", "-a", "--filter", `name=^${container.name}$`]);
|
package/content-config.mjs
CHANGED
|
@@ -69,7 +69,7 @@ import YAML from "yaml";
|
|
|
69
69
|
// Leaves with no local imports of their own, so naming them here cannot close
|
|
70
70
|
// a cycle around a consumer's config file (see `engine/pack-config.mjs`).
|
|
71
71
|
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-charset.mjs";
|
|
72
|
-
import { ASSET_TYPE_NAMES } from "./engine/asset-types.mjs";
|
|
72
|
+
import { ASSET_TYPE_NAMES, shippingLicensesProblem } from "./engine/asset-types.mjs";
|
|
73
73
|
import { isReservedPackage } from "./engine/packages.mjs";
|
|
74
74
|
import { EMPTY_ICON_REGISTRY, checkIconRegistry } from "./engine/content-icons.mjs";
|
|
75
75
|
import { MAP_TYPES, NO_PACK, PACK_BY_TYPE } from "./engine/ids.mjs";
|
|
@@ -2453,6 +2453,10 @@ function normalizePackageBuild(value) {
|
|
|
2453
2453
|
"reads, and that package validates what is inside it",
|
|
2454
2454
|
);
|
|
2455
2455
|
}
|
|
2456
|
+
// Read by the asset reference pass as well as by the packaging commands, so
|
|
2457
|
+
// a malformed list is refused wherever the configuration loads.
|
|
2458
|
+
const problem = shippingLicensesProblem(value.shippingLicenses);
|
|
2459
|
+
if (problem) fail("packageBuild.shippingLicenses", problem);
|
|
2456
2460
|
return /** @type {Readonly<PackageBuildSection>} */ (deepFreeze(structuredClone(value)));
|
|
2457
2461
|
}
|
|
2458
2462
|
|
package/docs/api.md
CHANGED
|
@@ -891,31 +891,67 @@ Emitting this package's content index. Every content build already walks the who
|
|
|
891
891
|
|
|
892
892
|
The asset types — `icon`, `image` and `audio` — and the three roots they are walked from. An asset is addressed exactly as a note is; what differs is that a `.webp` carries no frontmatter and has nowhere to say what it is, so the root supplies the type and the list of roots is closed.
|
|
893
893
|
|
|
894
|
-
| Export
|
|
895
|
-
|
|
|
896
|
-
| `ASSET_TYPES`
|
|
897
|
-
| `ASSET_TYPE_NAMES`
|
|
898
|
-
| `ASSET_SYSTEM`
|
|
899
|
-
| `IMAGE_EXTENSIONS`
|
|
900
|
-
| `AUDIO_EXTENSIONS`
|
|
901
|
-
| `isAssetType`
|
|
902
|
-
| `assetTypeOfRoot`
|
|
903
|
-
| `isAssetShortcode`
|
|
904
|
-
| `imageDimensions`
|
|
894
|
+
| Export | Signature | Returns | Use it when |
|
|
895
|
+
| ------------------------- | -------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
896
|
+
| `ASSET_TYPES` | `const ASSET_TYPES` | `readonly AssetType[]` — type, root, extensions, description | walking the roots, or presenting the vocabulary |
|
|
897
|
+
| `ASSET_TYPE_NAMES` | `const ASSET_TYPE_NAMES` | `ReadonlySet<string>` | admitting the asset types into a type vocabulary an address is parsed against |
|
|
898
|
+
| `ASSET_SYSTEM` | `const ASSET_SYSTEM` | `string` — always `none` | writing an asset address, whose `<system>` segment is a property of the type rather than of the reference |
|
|
899
|
+
| `IMAGE_EXTENSIONS` | `const IMAGE_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | deciding whether a file under a picture root is an asset at all |
|
|
900
|
+
| `AUDIO_EXTENSIONS` | `const AUDIO_EXTENSIONS` | `readonly string[]` — lowercase, dot-led | the same question for `audio/` |
|
|
901
|
+
| `isAssetType` | `isAssetType(type)` | `boolean` | scoping a rewrite rule, which may substitute a file and nothing else |
|
|
902
|
+
| `assetTypeOfRoot` | `assetTypeOfRoot(root)` | `AssetType \| undefined` | reading a root directory name back to the type it declares |
|
|
903
|
+
| `isAssetShortcode` | `isAssetShortcode(shortcode)` | `boolean` | telling an addressable filename from one that carries a version string, a hyphen or a date stamp |
|
|
904
|
+
| `imageDimensions` | `imageDimensions(absPath, ext)` | `{width, height} \| undefined` | reading a raster file's own pixel size from its header, which an SVG carries none of and a partial file fails closed on |
|
|
905
|
+
| `DEPLOYED_ASSET_TYPES` | `const DEPLOYED_ASSET_TYPES` | `readonly AssetType[]` — `icon` and `image` | naming the types a location holds flat, which audio is not |
|
|
906
|
+
| `isDeployedSuffix` | `isDeployedSuffix(suffix)` | `boolean` | asking whether a path inside `assets/` sits under a picture root, and so deploys flat |
|
|
907
|
+
| `deployedSuffix` | `deployedSuffix(suffix)` | `string` — `<icons\|images>/<basename>`, or the suffix unchanged | turning a source path inside `assets/` into the path the file has in the package, on the CDN and in every location |
|
|
908
|
+
| `shippingLicensesProblem` | `shippingLicensesProblem(value)` | `string` — a clause, or `""` | checking a `packageBuild.shippingLicenses` value: a list of non-empty, distinct strings |
|
|
905
909
|
|
|
906
910
|
### `engine.assetIndex`
|
|
907
911
|
|
|
908
|
-
The asset record: one line of the content index per addressable file. The record is emitted by the package holding the bytes, so its `path` is that package's path and
|
|
909
|
-
|
|
910
|
-
| Export | Signature | Returns | Use it when
|
|
911
|
-
| --------------------------- | --------------------------------------------------------------- | ----------------------------- |
|
|
912
|
-
| `ASSET_ROLES` | `const ASSET_ROLES` | `readonly string[]` | naming the closed set of roles a picture may declare
|
|
913
|
-
| `ASSET_RECORD_FIELDS` | `const ASSET_RECORD_FIELDS` | `readonly AssetRecordField[]` | reading what an `asset` block carries, and where each field comes from
|
|
914
|
-
| `PROVENANCE_KEYS` | `const PROVENANCE_KEYS` | `ReadonlySet<string>` | validating a provenance file, whose keys are derived from the record's own fields
|
|
915
|
-
| `REQUIRED_PROVENANCE_KEYS` | `const REQUIRED_PROVENANCE_KEYS` | `ReadonlySet<string>` | the keys a provenance file must state — a record resolves wholesale, so one omitting them leaves every file it covers
|
|
916
|
-
| `PROVENANCE_FILE` | `const PROVENANCE_FILE` | `string` — `provenance.yaml` | naming the file a directory records provenance for its subtree in
|
|
917
|
-
| `PROVENANCE_SIDECAR_SUFFIX` | `const PROVENANCE_SIDECAR_SUFFIX` | `string` — `.yaml` | naming a per-file record, which replaces an inherited one wholesale
|
|
918
|
-
| `collectAssetRecords` | `collectAssetRecords(assetsBase, { contentPackage, problems })` | `Array<Record<string, any>>` | reading a package's asset roots into index records, without walking its notes
|
|
912
|
+
The asset record: one line of the content index per addressable file. The record is emitted by the package holding the bytes, so its `path` is that package's source path inside `assets/`, which the book stages from; Foundry and the website serve the deployed layout derived from it (`engine.assetShipping`). Every asset is covered by a provenance record that states `attribution`, `license` and `ai`, and a file no record covers is a finding.
|
|
913
|
+
|
|
914
|
+
| Export | Signature | Returns | Use it when |
|
|
915
|
+
| --------------------------- | --------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
916
|
+
| `ASSET_ROLES` | `const ASSET_ROLES` | `readonly string[]` | naming the closed set of roles a picture may declare |
|
|
917
|
+
| `ASSET_RECORD_FIELDS` | `const ASSET_RECORD_FIELDS` | `readonly AssetRecordField[]` | reading what an `asset` block carries, and where each field comes from |
|
|
918
|
+
| `PROVENANCE_KEYS` | `const PROVENANCE_KEYS` | `ReadonlySet<string>` | validating a provenance file, whose keys are derived from the record's own fields |
|
|
919
|
+
| `REQUIRED_PROVENANCE_KEYS` | `const REQUIRED_PROVENANCE_KEYS` | `ReadonlySet<string>` | the keys a provenance file must state — `attribution`, `license` and `ai`; a record resolves wholesale, so one omitting them leaves every file it covers without that answer |
|
|
920
|
+
| `PROVENANCE_FILE` | `const PROVENANCE_FILE` | `string` — `provenance.yaml` | naming the file a directory records provenance for its subtree in |
|
|
921
|
+
| `PROVENANCE_SIDECAR_SUFFIX` | `const PROVENANCE_SIDECAR_SUFFIX` | `string` — `.yaml` | naming a per-file record, which replaces an inherited one wholesale |
|
|
922
|
+
| `collectAssetRecords` | `collectAssetRecords(assetsBase, { contentPackage, problems })` | `Array<Record<string, any>>` | reading a package's asset roots into index records, without walking its notes |
|
|
923
|
+
|
|
924
|
+
### `engine.assetShipping`
|
|
925
|
+
|
|
926
|
+
The deployed layout of a package's pictures, and which of them ship. A Foundry package holds its icons and images flat — `icons/<basename>`, `images/<basename>` — and the stage copies a picture only when its provenance says `ai: false` and its licence is on the shipping list. Its asset set, `asset-sets/default.json`, lists what it ships. The selection is one function, so the stage and any command publishing what the stage leaves out read the same answer.
|
|
927
|
+
|
|
928
|
+
| Export | Signature | Returns | Use it when |
|
|
929
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
930
|
+
| `SHIPPING_LICENSES` | `const SHIPPING_LICENSES` | `readonly string[]` | reading the licences a shipped picture may carry, exactly as provenance spells them |
|
|
931
|
+
| `SHIPPED_SET_FILE` | `const SHIPPED_SET_FILE` | `string` — `asset-sets/default.json` | naming the staged package's own asset set, relative to the package root |
|
|
932
|
+
| `STAND_IN_SHORTCODE` | `const STAND_IN_SHORTCODE` | `string` — `missing` | naming the shortcode of a package's stand-in picture |
|
|
933
|
+
| `DEPLOYED_ASSET_TYPES` | `const DEPLOYED_ASSET_TYPES` | `readonly AssetType[]` | re-exported from `engine.assetTypes` |
|
|
934
|
+
| `deployedSuffix` | `deployedSuffix(suffix)` | `string` | re-exported from `engine.assetTypes` |
|
|
935
|
+
| `deployedAssetPath` | `deployedAssetPath(record)` | `string \| undefined` — `<icons\|images>/<basename>` | reading the deployed path of one asset record; `undefined` for audio |
|
|
936
|
+
| `exclusionReason` | `exclusionReason(record, additions?)` | `string` — a clause, or `""` | saying why a picture does not ship |
|
|
937
|
+
| `shipsAsset` | `shipsAsset(record, additions?)` | `boolean` | asking whether one picture ships |
|
|
938
|
+
| `standInAddress` | `standInAddress(pkg, type)` | `string` — `<package>-none-<type>-missing` | naming the stand-in an excluded picture compiles to |
|
|
939
|
+
| `selectStagedAssets` | `selectStagedAssets(records, additions?)` | `{staged, excluded}` — both in address order, each excluded one with its reason | splitting a package's pictures into what ships and what does not |
|
|
940
|
+
| `shippedSetName` | `shippedSetName(title)` | `string` — `<title> — shipped art` | naming the staged package's own asset set |
|
|
941
|
+
| `assetsManifest` | `assetsManifest(staged, stageDir)` | `{address, source, target, size}[]` | listing staged files as the shipped set does, each `source` relative to `asset-sets/`, sizes measured in the stage |
|
|
942
|
+
| `stagePackageAssets` | `stagePackageAssets({ assetsBase, contentPackage, stageDir, title, transform, shippingLicenses })` | `{staged, excluded, findings, manifest}` | staging a package's shipped pictures flat and writing `asset-sets/default.json`; nothing is staged when `findings` is non-empty |
|
|
943
|
+
| `rowsStagingPictureRoots` | `rowsStagingPictureRoots(rows, { rootDir, assetsBase })` | `{index, from, root}[]` | finding `packageBuild.assets` rows that would copy a picture root around the filter |
|
|
944
|
+
|
|
945
|
+
### `engine.assetReferences`
|
|
946
|
+
|
|
947
|
+
Every asset reference in a compiled document states the asset's address: an `<img>` carries `data-asset`, and a path field's address sits in `flags.<system>.assets` on the document holding it, nested to mirror the field's path. A reference to a picture its package does not ship compiles to that package's stand-in and keeps the excluded address.
|
|
948
|
+
|
|
949
|
+
| Export | Signature | Returns | Use it when |
|
|
950
|
+
| ------------------------- | ------------------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
951
|
+
| `ASSET_PATH_FIELDS` | `const ASSET_PATH_FIELDS` | `Record<string, AssetPathField[]>` | reading which fields of which document types name a picture, and where each records its address |
|
|
952
|
+
| `assetReferenceLookup` | `assetReferenceLookup(linkIndex, config)` | `{byPath, byAddress}` | indexing every picture a compiled document can name, local and fetched, by Foundry path and by address |
|
|
953
|
+
| `annotateHtml` | `annotateHtml(html, lookup, report)` | `string` | marking each `<img>` in one HTML string with its address |
|
|
954
|
+
| `annotateAssetReferences` | `annotateAssetReferences(doc, documentType, { lookup, system, report })` | `object` — the document, changed in place | stating every reference's address on a finished document and drawing stand-ins |
|
|
919
955
|
|
|
920
956
|
### `engine.packages`
|
|
921
957
|
|
|
@@ -1719,36 +1755,48 @@ console.log(dataEnvVar("dev"));
|
|
|
1719
1755
|
// -> "FOUNDRYVTT_DEV_DATA"
|
|
1720
1756
|
```
|
|
1721
1757
|
|
|
1722
|
-
| Export | Signature
|
|
1723
|
-
| -------------------------- |
|
|
1724
|
-
| `DEFAULT_STAGE_PORTS` | `const DEFAULT_STAGE_PORTS`
|
|
1725
|
-
| `CONTAINER_PORT` | `const CONTAINER_PORT`
|
|
1726
|
-
| `CACHE_MOUNT` | `const CACHE_MOUNT`
|
|
1727
|
-
| `CONTAINER_ACTIONS` | `const CONTAINER_ACTIONS`
|
|
1728
|
-
| `dataEnvVar` | `dataEnvVar(stage)`
|
|
1729
|
-
| `containerName` | `containerName(packageId, stage, base = null)`
|
|
1730
|
-
| `resolveStagePort` | `resolveStagePort(stage, { env, stages } = {})`
|
|
1731
|
-
| `resolveFoundryVersion` | `resolveFoundryVersion(stage, { env, stages, compatibilityMinimum, e2eStage = "test" } = {})`
|
|
1732
|
-
| `resolveImage` | `resolveImage({ env, image, version, compatibilityMinimum } = {})`
|
|
1733
|
-
| `resolveWorld` | `resolveWorld(stage, { env, stages } = {})`
|
|
1734
|
-
| `resolveLicenseKey` | `resolveLicenseKey(stage, env = process.env)`
|
|
1735
|
-
| `resolveDataRoot` | `resolveDataRoot(stage, { env } = {})`
|
|
1736
|
-
| `passthroughEnv` | `passthroughEnv(env = process.env)`
|
|
1737
|
-
| `needsFoundryAccount` | `needsFoundryAccount({ cacheDir, dataRoot, version, licenseKey, fileExists })`
|
|
1738
|
-
| `dockerRunArgs` | `dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, version, world, licenseKey, account, user })` | `string[]` | building the full `docker run` argument vector for a stage's container; `account: false` withholds the foundryvtt.com account |
|
|
1739
|
-
| `runDocker` | `runDocker(args)`
|
|
1740
|
-
| `captureDocker` | `captureDocker(args)`
|
|
1741
|
-
| `dataRootScriptArgs` | `dataRootScriptArgs({ image, dataRoot, script, args, user })`
|
|
1742
|
-
| `runDataRootScript` | `runDataRootScript(opts)`
|
|
1743
|
-
| `containerTreeRemover` | `containerTreeRemover({ container, runScript })`
|
|
1744
|
-
| `containerRemovalCommand` | `containerRemovalCommand({ image, dataRoot, tree })`
|
|
1745
|
-
| `containerExists` | `containerExists(name, runningOnly = false)`
|
|
1746
|
-
| `runningFoundryContainers` | `runningFoundryContainers(except = "")`
|
|
1747
|
-
| `clearStaleLock` | `clearStaleLock(dataRoot, log = () => {})`
|
|
1748
|
-
| `
|
|
1749
|
-
| `
|
|
1750
|
-
| `
|
|
1751
|
-
| `
|
|
1758
|
+
| Export | Signature | Returns | Use it when |
|
|
1759
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1760
|
+
| `DEFAULT_STAGE_PORTS` | `const DEFAULT_STAGE_PORTS` | — | reading the host port each conventional stage (`dev`/`qa`/`prod`/`test`) publishes by default, chosen so all four can run at once |
|
|
1761
|
+
| `CONTAINER_PORT` | `const CONTAINER_PORT` | — | the fixed port Foundry listens on inside the container |
|
|
1762
|
+
| `CACHE_MOUNT` | `const CACHE_MOUNT` | — | the path a host-provided download cache is mounted at inside the container |
|
|
1763
|
+
| `CONTAINER_ACTIONS` | `const CONTAINER_ACTIONS` | — | the closed set of actions `package-build container` accepts |
|
|
1764
|
+
| `dataEnvVar` | `dataEnvVar(stage)` | `string` | deriving the environment variable that names a stage's Foundry data root, without hand-tabulating one per stage |
|
|
1765
|
+
| `containerName` | `containerName(packageId, stage, base = null)` | `string` | computing the stable container name a stage runs under, so a signed Foundry licence (bound to the hostname) survives a recreate; pass `base` to share one container/licence across packages |
|
|
1766
|
+
| `resolveStagePort` | `resolveStagePort(stage, { env, stages } = {})` | `number` | resolving the host port for a stage, honoring `FOUNDRYVTT_<STAGE>_PORT` over a declared or conventional default |
|
|
1767
|
+
| `resolveFoundryVersion` | `resolveFoundryVersion(stage, { env, stages, compatibilityMinimum, e2eStage = "test" } = {})` | `string\|null` | pinning the exact Foundry build a stage runs, deriving the end-to-end stage's pin from the package's own `compatibility.minimum` so the floor is actually exercised |
|
|
1768
|
+
| `resolveImage` | `resolveImage({ env, image, version, compatibilityMinimum } = {})` | `string` | resolving the `felddy/foundryvtt` image reference a run uses — the pinned build's own tag when a build is pinned |
|
|
1769
|
+
| `resolveWorld` | `resolveWorld(stage, { env, stages } = {})` | `string\|null` | resolving which world a stage auto-launches — `""` declares "never auto-launch", distinct from `null` (leave `FOUNDRY_WORLD` alone) |
|
|
1770
|
+
| `resolveLicenseKey` | `resolveLicenseKey(stage, env = process.env)` | `string\|null` | finding a stage's dedicated Foundry licence key, since Foundry is single-seat and two stages running at once need two keys |
|
|
1771
|
+
| `resolveDataRoot` | `resolveDataRoot(stage, { env } = {})` | `string` | resolving the local Foundry data root path for a stage, checked for what a bind mount needs |
|
|
1772
|
+
| `passthroughEnv` | `passthroughEnv(env = process.env)` | `[string, string][]` | collecting the image's own `FOUNDRY_*`/`CONTAINER_*` environment pairs to pass through (deliberately excludes `CONTAINER_CACHE`, which `dockerRunArgs` sets itself) |
|
|
1773
|
+
| `needsFoundryAccount` | `needsFoundryAccount({ cacheDir, dataRoot, version, licenseKey, fileExists })` | `boolean` | deciding whether the image needs `FOUNDRY_USERNAME`/`FOUNDRY_PASSWORD`: only when the pinned build's archive is not cached, or the licence must come from the account |
|
|
1774
|
+
| `dockerRunArgs` | `dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, version, world, licenseKey, account, network, awsConfig, user })` | `string[]` | building the full `docker run` argument vector for a stage's container; `account: false` withholds the foundryvtt.com account |
|
|
1775
|
+
| `runDocker` | `runDocker(args)` | `number` | running the `docker` CLI with inherited stdio and getting back its exit status |
|
|
1776
|
+
| `captureDocker` | `captureDocker(args)` | `string` | running `docker` and capturing trimmed stdout, tolerating failure (returns `""`) |
|
|
1777
|
+
| `dataRootScriptArgs` | `dataRootScriptArgs({ image, dataRoot, script, args, user })` | `string[]` | building the `docker run` vector for a throwaway container that runs a shell script against a stage's data root, mounted at `/data` |
|
|
1778
|
+
| `runDataRootScript` | `runDataRootScript(opts)` | `string` | running that script and getting its stdout, throwing on a non-zero exit; the way to remove or hand over files that the Foundry container's user owns |
|
|
1779
|
+
| `containerTreeRemover` | `containerTreeRemover({ container, runScript })` | `(tree, { dataRoot }) => void` | removing a tree beneath a stage's data root that the Foundry container's user owns, as root in a throwaway container; the remover `package-build deploy` passes to `deployStage` |
|
|
1780
|
+
| `containerRemovalCommand` | `containerRemovalCommand({ image, dataRoot, tree })` | `string` | writing out the `docker run` command that removes such a tree, for a person to paste |
|
|
1781
|
+
| `containerExists` | `containerExists(name, runningOnly = false)` | `boolean` | checking whether a container with exactly this name exists (optionally, only counting a running one) |
|
|
1782
|
+
| `runningFoundryContainers` | `runningFoundryContainers(except = "")` | `string[]` | listing HeroicLands-convention Foundry containers currently running, to warn about a licence clash before a run starts |
|
|
1783
|
+
| `clearStaleLock` | `clearStaleLock(dataRoot, log = () => {})` | — | removing a data-root lock left behind by a container that did not shut down cleanly; only safe to call while the container is stopped |
|
|
1784
|
+
| `S3_IMAGE` | `const S3_IMAGE` | `string` | reading the S3 server's image |
|
|
1785
|
+
| `S3_ALIAS` | `const S3_ALIAS` | `string` | reading the name the S3 server answers to on the network |
|
|
1786
|
+
| `S3_PORT` | `const S3_PORT` | `number` | reading the port the S3 server publishes |
|
|
1787
|
+
| `S3_BUCKET` | `const S3_BUCKET` | `string` | reading the bucket the S3 server is created with |
|
|
1788
|
+
| `S3_IDENTITIES_FILE` | `const S3_IDENTITIES_FILE` | `string` | reading the repository-relative SeaweedFS identities file the S3 opt-in takes |
|
|
1789
|
+
| `S3_AWS_CONFIG_FILE` | `const S3_AWS_CONFIG_FILE` | `string` | reading the repository-relative Foundry `awsConfig` the S3 opt-in takes |
|
|
1790
|
+
| `s3Requested` | `s3Requested(env = process.env)` | `boolean` | asking whether `FOUNDRYVTT_CONTAINER_S3` opts in |
|
|
1791
|
+
| `resolveS3` | `resolveS3({ name, rootDir, env })` | `S3Plan\|null` | planning the network, server and files of the S3 opt-in; `null` when off |
|
|
1792
|
+
| `s3RunArgs` | `s3RunArgs(plan)` | `string[]` | building the S3 server's `docker run` vector |
|
|
1793
|
+
| `s3ShellArgs` | `s3ShellArgs(plan, command)` | `string[]` | building the `docker exec` vector running one `weed shell` command |
|
|
1794
|
+
| `ensureS3` | `ensureS3({ plan, dataRoot, log, run, capture, sleep, attempts })` | `void` | starting the network, server and bucket and copying the `awsConfig` into the data root |
|
|
1795
|
+
| `removeS3` | `removeS3(plan, run, capture)` | `void` | removing the S3 server and its network |
|
|
1796
|
+
| `resolveContainer` | `resolveContainer({ stage, config, env = process.env })` | `ResolvedContainer` | resolving all of a stage's container settings in one call, from the package-build configuration |
|
|
1797
|
+
| `startContainer` | `startContainer(container, { dataRoot, env, log })` | `number` | starting a stage's container, creating it first if it does not exist |
|
|
1798
|
+
| `removeContainer` | `removeContainer(name, log = () => {})` | — | stopping and removing a container, tolerating "not running" and "no such container" |
|
|
1799
|
+
| `containerAction` | `containerAction({ action, stage, config, env, log })` | `number` | performing one `CONTAINER_ACTIONS` action for a stage end to end |
|
|
1752
1800
|
|
|
1753
1801
|
## `./coverage`
|
|
1754
1802
|
|
package/docs/authoring/assets.md
CHANGED
|
@@ -26,16 +26,31 @@ The embed's label is alternative text. `![[icon-harbor|]]` is decorative. `size=
|
|
|
26
26
|
|
|
27
27
|
## Ownership and source records
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Every asset is covered by a provenance record. Put `provenance.yaml` in an asset directory to describe files there and below, or put a YAML sidecar named for the whole filename — `anvil.svg.yaml` — beside one asset to give that file its own record. A sidecar replaces the directory record for that asset; otherwise the nearest `provenance.yaml` in the file's directory or one above it, up to the type root, answers. Each file resolves on its own, so a directory may hold files its `provenance.yaml` covers beside files a sidecar answers for. A file no record covers is an error at the file. Keep attribution with the authored asset so movement through the tree does not lose its source or license. See the [asset record reference](../reference/format-details.md#the-asset-record) for the accepted keys, inheritance, and examples.
|
|
30
|
+
|
|
31
|
+
Every record states `attribution`, `license` and `ai` — whether a machine made the file, as the YAML boolean `true` or `false`. A record that leaves `ai` out is an error at the record; it is never read as `false`.
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
attribution: Tom Rodriguez
|
|
35
|
+
license: CC-BY-SA-4.0
|
|
36
|
+
ai: false
|
|
37
|
+
```
|
|
30
38
|
|
|
31
39
|
A directory of portraits also states what the pictures in it are for:
|
|
32
40
|
|
|
33
41
|
```yaml
|
|
34
42
|
attribution: Tom Rodriguez
|
|
35
43
|
license: CC-BY-SA-4.0
|
|
44
|
+
ai: false
|
|
36
45
|
role: portrait
|
|
37
46
|
```
|
|
38
47
|
|
|
48
|
+
## What the Foundry package ships
|
|
49
|
+
|
|
50
|
+
The repository keeps every picture; the Foundry package carries a filtered copy, laid out flat. A picture ships when its record says `ai: false` and its licence is one of `LicenseRef-PublicDomain`, `CC0-1.0`, `CC0`, `CC-BY-3.0`, `CC-BY-4.0`, `CC-BY-SA-4.0` or `LicenseRef-NounProject-Pro`, together with any licence the package adds under `packageBuild.shippingLicenses`. A shipped picture sits at `icons/<basename>` or `images/<basename>` at the package root, whatever directories it is kept in here, and the package's asset set, `asset-sets/default.json`, lists what it ships.
|
|
51
|
+
|
|
52
|
+
A note may still name a picture the package does not ship. Its compiled document draws the package's stand-in instead — `icons/missing.<ext>` or `images/missing.<ext>`, which you provide as an ordinary human-made picture licensed to ship, addressed `<package>-none-icon-missing` and `<package>-none-image-missing` — and keeps the real picture's address, so a world that holds the real file can point back at it. A package that leaves out a picture a note names, and ships no stand-in of that type, fails the build at the note. See [what a package ships](../reference/format-details.md#what-a-package-ships).
|
|
53
|
+
|
|
39
54
|
**A hero image is 1792×768.** `data.banner` fills one fixed strip wherever it is drawn, so every banner is cut to that size and a picture of any other size is a finding naming both sizes — resize the picture to match. That is the one size any art field requires.
|
|
40
55
|
|
|
41
56
|
`data.banner` reads in one of three states:
|