@heroiclands/package-build 8.1.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +15 -2
- package/types/engine/helpers.d.mts +22 -14
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/manifest.d.mts +1 -1
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/content-config.mjs
CHANGED
|
@@ -66,9 +66,10 @@
|
|
|
66
66
|
|
|
67
67
|
import path from "node:path";
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
// cycle around a consumer's config file (see `engine/pack-config.mjs`).
|
|
71
|
-
import {
|
|
69
|
+
// Leaves with no local imports of their own, so naming them here cannot close
|
|
70
|
+
// a cycle around a consumer's config file (see `engine/pack-config.mjs`).
|
|
71
|
+
import { isAddressSegment } from "./engine/address-charset.mjs";
|
|
72
|
+
import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
75
|
* The two kinds of Foundry package a content module can be built into. The
|
|
@@ -470,6 +471,18 @@ export function publishesContentPages(config) {
|
|
|
470
471
|
* @typedef {((fm: object) => object)|{system: (fm: object) => object, img?: string, fields?: readonly object[]}} ItemBuilderEntry
|
|
471
472
|
*/
|
|
472
473
|
|
|
474
|
+
/**
|
|
475
|
+
* One **registry** of a declared set, and the system it belongs to (#58).
|
|
476
|
+
*
|
|
477
|
+
* A repository shipping content for two systems declares one of these per
|
|
478
|
+
* system: the accepted type vocabulary is their union, and a type both declare
|
|
479
|
+
* keeps a builder on each side rather than one of them winning in silence.
|
|
480
|
+
*
|
|
481
|
+
* @typedef {object} ItemRegistrySpec
|
|
482
|
+
* @property {string} system The system id whose vocabulary this registry is.
|
|
483
|
+
* @property {Record<string, ItemBuilderEntry>} builders The registry itself.
|
|
484
|
+
*/
|
|
485
|
+
|
|
473
486
|
/**
|
|
474
487
|
* The configuration a consumer writes.
|
|
475
488
|
*
|
|
@@ -485,7 +498,8 @@ export function publishesContentPages(config) {
|
|
|
485
498
|
* `system.json` / `module.json`.
|
|
486
499
|
* @property {PackageKind} packageKind Whether the package is a system or a module.
|
|
487
500
|
* @property {StatsSpec} stats Identity stamped into every document's `_stats`.
|
|
488
|
-
* @property {Record<string, ItemBuilderEntry
|
|
501
|
+
* @property {Record<string, ItemBuilderEntry>|readonly ItemRegistrySpec[]} [itemBuilders]
|
|
502
|
+
* The consumer's
|
|
489
503
|
* item-type registry: each content `type`
|
|
490
504
|
* that compiles into an Item, paired with
|
|
491
505
|
* the builder producing its `system` block
|
|
@@ -493,7 +507,11 @@ export function publishesContentPages(config) {
|
|
|
493
507
|
* note of that type gets when it sets no
|
|
494
508
|
* `img:` of its own. Default `{}` — a
|
|
495
509
|
* content module that ships no items
|
|
496
|
-
* declares none.
|
|
510
|
+
* declares none. A repository feeding
|
|
511
|
+
* two systems declares a **list** of
|
|
512
|
+
* `{ system, builders }` registries
|
|
513
|
+
* instead, and the accepted type
|
|
514
|
+
* vocabulary is their union (#58).
|
|
497
515
|
* @property {PackSpec[]} packs Packs to compile. More than one entry
|
|
498
516
|
* may share a `type`: a note then names
|
|
499
517
|
* the pack it belongs in with its
|
|
@@ -547,10 +565,24 @@ export function publishesContentPages(config) {
|
|
|
547
565
|
* declared. Sparse, like `itemArt` — a type
|
|
548
566
|
* absent here compiles normally and is
|
|
549
567
|
* simply undocumented (#22).
|
|
568
|
+
* @property {Readonly<Record<string, Readonly<Record<string, Function>>>>} itemBuildersBySystem
|
|
569
|
+
* Derived: the same builders, kept per
|
|
570
|
+
* declaring system. `{}` for the single
|
|
571
|
+
* registry form, which names no system
|
|
572
|
+
* (#58).
|
|
573
|
+
* @property {Readonly<Record<string, Readonly<Record<string, string>>>>} itemArtBySystem
|
|
574
|
+
* Derived: the default art, per system.
|
|
575
|
+
* @property {Readonly<Record<string, Readonly<Record<string, readonly object[]>>>>} itemFieldsBySystem
|
|
576
|
+
* Derived: the declared fields, per system.
|
|
577
|
+
* @property {ReadonlySet<string>} itemTypesBySeveralSystems Derived: the types
|
|
578
|
+
* more than one registry declares — the
|
|
579
|
+
* ones the flat tables cannot answer for
|
|
580
|
+
* without choosing a system for the caller.
|
|
550
581
|
* @property {ReadonlySet<string>} itemTypes Derived: the keys of
|
|
551
582
|
* {@link ContentBuildConfigInput.itemBuilders},
|
|
552
|
-
*
|
|
553
|
-
*
|
|
583
|
+
* unioned across every declared registry, so
|
|
584
|
+
* the accepted item types and the builder
|
|
585
|
+
* tables are one list (#1504).
|
|
554
586
|
* @property {ReadonlySet<string>} docEntryTypes Derived: every type whose prose
|
|
555
587
|
* compiles into a JournalEntry of its own —
|
|
556
588
|
* the item types, plus `macro`, plus the map
|
|
@@ -608,14 +640,9 @@ const SITE_TREE_KEYS = ["from", "section"];
|
|
|
608
640
|
const SECTION_META_KEYS = ["title", "banner", "description"];
|
|
609
641
|
const DOC_PAGE_KEYS = ["title", "out", "preamble"];
|
|
610
642
|
const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
|
|
611
|
-
const RELATIONSHIP_KEYS = [
|
|
612
|
-
"id",
|
|
613
|
-
"type",
|
|
614
|
-
"manifest",
|
|
615
|
-
"compatibility",
|
|
616
|
-
"itemCatalog",
|
|
617
|
-
];
|
|
643
|
+
const RELATIONSHIP_KEYS = ["id", "type", "manifest", "compatibility", "itemCatalog"];
|
|
618
644
|
const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
|
|
645
|
+
const ITEM_REGISTRY_KEYS = ["system", "builders"];
|
|
619
646
|
const PACK_KEYS = [
|
|
620
647
|
"name",
|
|
621
648
|
"type",
|
|
@@ -644,9 +671,7 @@ const STATS_KEYS = ["lastModifiedBy"];
|
|
|
644
671
|
*
|
|
645
672
|
* @type {symbol}
|
|
646
673
|
*/
|
|
647
|
-
export const DERIVED_SYSTEM_VERSION = Symbol.for(
|
|
648
|
-
"package-build.derivedSystemVersion",
|
|
649
|
-
);
|
|
674
|
+
export const DERIVED_SYSTEM_VERSION = Symbol.for("package-build.derivedSystemVersion");
|
|
650
675
|
const PUBLISH_KEYS = ["site", "manifests", "address"];
|
|
651
676
|
const MANIFEST_KEYS = ["publish", "consume"];
|
|
652
677
|
const ADDRESS_KEYS = ["prefix", "landing"];
|
|
@@ -672,10 +697,7 @@ function isPlainObject(value) {
|
|
|
672
697
|
* @returns {never}
|
|
673
698
|
*/
|
|
674
699
|
function fail(field, problem) {
|
|
675
|
-
throw Object.assign(
|
|
676
|
-
new TypeError(`package-build config: \`${field}\` ${problem}.`),
|
|
677
|
-
{ field },
|
|
678
|
-
);
|
|
700
|
+
throw Object.assign(new TypeError(`package-build config: \`${field}\` ${problem}.`), { field });
|
|
679
701
|
}
|
|
680
702
|
|
|
681
703
|
/**
|
|
@@ -706,6 +728,65 @@ function requireNonEmptyString(value, field) {
|
|
|
706
728
|
return /** @type {string} */ (value);
|
|
707
729
|
}
|
|
708
730
|
|
|
731
|
+
/**
|
|
732
|
+
* The `contentPackage`, checked against the two rules an address puts on it.
|
|
733
|
+
*
|
|
734
|
+
* It is the first segment of every canonical address this repository publishes
|
|
735
|
+
* (`sohl-skill-clmb`), and an address is read by counting hyphen-separated
|
|
736
|
+
* segments. So the value carries two obligations that the rest of the
|
|
737
|
+
* configuration does not, and #59 asks for both to be **enforced rather than
|
|
738
|
+
* assumed** — the alternative is a package whose addresses are simply
|
|
739
|
+
* unreadable, reported nowhere and discovered as links that resolve to nothing.
|
|
740
|
+
*
|
|
741
|
+
* 1. _Alphanumeric_, so the hyphen stays purely a separator. `harn-adventures`
|
|
742
|
+
* was the one violator, and its keys read as four segments and failed as a
|
|
743
|
+
* `null` return from `readCanonicalKey` — a silence, not an error.
|
|
744
|
+
* 2. _Not a note type_, because the package and the type are adjacent segments
|
|
745
|
+
* drawn from two vocabularies. Keeping them disjoint is what lets a reader
|
|
746
|
+
* take a name at face value instead of deciding which slot it is filling.
|
|
747
|
+
* One such collision is structural and cannot be fixed — `sohl` is both a
|
|
748
|
+
* content package and a system id, because Foundry requires a system
|
|
749
|
+
* package's id to *be* its system id — which is the reason to prevent the
|
|
750
|
+
* ones that are avoidable.
|
|
751
|
+
*
|
|
752
|
+
* @param {unknown} value - The configured `contentPackage`.
|
|
753
|
+
* @param {ReadonlySet<string>} docEntryTypes - Every type whose prose compiles
|
|
754
|
+
* to a documentation entry: the item types plus `macro` and the map types.
|
|
755
|
+
* With {@link PACK_BY_TYPE} and the `doc`-prefixed forms, this is the whole
|
|
756
|
+
* type vocabulary an address may write.
|
|
757
|
+
* @returns {string} The value, unchanged.
|
|
758
|
+
*/
|
|
759
|
+
function requireContentPackage(value, docEntryTypes) {
|
|
760
|
+
const pkg = requireNonEmptyString(value, "contentPackage");
|
|
761
|
+
if (!isAddressSegment(pkg)) {
|
|
762
|
+
fail(
|
|
763
|
+
"contentPackage",
|
|
764
|
+
`is \`${pkg}\`, which is not alphanumeric. It is the first ` +
|
|
765
|
+
`segment of every address this package publishes ` +
|
|
766
|
+
`(\`${pkg}-<type>-<shortcode>\`), and an address is read by ` +
|
|
767
|
+
`counting hyphen-separated segments — so anything outside ` +
|
|
768
|
+
"`[A-Za-z0-9]` here makes those addresses unreadable rather " +
|
|
769
|
+
"than merely ugly. `harn-adventures` became `harnadventures`",
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const typeNames = new Set([
|
|
773
|
+
...Object.keys(PACK_BY_TYPE),
|
|
774
|
+
...docEntryTypes,
|
|
775
|
+
...[...docEntryTypes].map((type) => `doc${type}`),
|
|
776
|
+
]);
|
|
777
|
+
if (typeNames.has(pkg)) {
|
|
778
|
+
fail(
|
|
779
|
+
"contentPackage",
|
|
780
|
+
`is \`${pkg}\`, which is also a note type — \`${pkg}-<shortcode>\` ` +
|
|
781
|
+
"already addresses one. The package and the type are adjacent " +
|
|
782
|
+
"segments of an address, and the two vocabularies are kept " +
|
|
783
|
+
"disjoint so a reader never has to decide which slot a name " +
|
|
784
|
+
"is filling. Rename the package",
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
return pkg;
|
|
788
|
+
}
|
|
789
|
+
|
|
709
790
|
/**
|
|
710
791
|
* @param {unknown} value
|
|
711
792
|
* @param {string} field
|
|
@@ -752,10 +833,7 @@ function normalizePack(value, where, nested = false) {
|
|
|
752
833
|
typeof type !== "string" ||
|
|
753
834
|
!(/** @type {readonly string[]} */ (PACK_DOCUMENT_TYPES).includes(type))
|
|
754
835
|
) {
|
|
755
|
-
fail(
|
|
756
|
-
`${where}.type`,
|
|
757
|
-
`must be one of: ${PACK_DOCUMENT_TYPES.join(", ")}`,
|
|
758
|
-
);
|
|
836
|
+
fail(`${where}.type`, `must be one of: ${PACK_DOCUMENT_TYPES.join(", ")}`);
|
|
759
837
|
}
|
|
760
838
|
|
|
761
839
|
if (pack.folders !== undefined && pack.folders !== null) {
|
|
@@ -837,20 +915,14 @@ function normalizePack(value, where, nested = false) {
|
|
|
837
915
|
name,
|
|
838
916
|
type: /** @type {PackDocumentType} */ (type),
|
|
839
917
|
label:
|
|
840
|
-
pack.label === undefined ?
|
|
841
|
-
name
|
|
842
|
-
: requireNonEmptyString(pack.label, `${where}.label`),
|
|
918
|
+
pack.label === undefined ? name : requireNonEmptyString(pack.label, `${where}.label`),
|
|
843
919
|
private: optionalBoolean(pack.private, `${where}.private`, false),
|
|
844
920
|
folders:
|
|
845
921
|
pack.folders === undefined || pack.folders === null ?
|
|
846
922
|
null
|
|
847
923
|
: /** @type {string} */ (pack.folders),
|
|
848
924
|
companions: Object.freeze(companions),
|
|
849
|
-
mayBeEmpty: optionalBoolean(
|
|
850
|
-
pack.mayBeEmpty,
|
|
851
|
-
`${where}.mayBeEmpty`,
|
|
852
|
-
false,
|
|
853
|
-
),
|
|
925
|
+
mayBeEmpty: optionalBoolean(pack.mayBeEmpty, `${where}.mayBeEmpty`, false),
|
|
854
926
|
// Which pack of a type receives a note that declares none. Validated
|
|
855
927
|
// across the whole list in `defineConfig` — at most one per type.
|
|
856
928
|
default: optionalBoolean(pack.default, `${where}.default`, false),
|
|
@@ -938,10 +1010,7 @@ function normalizeStats(value, derived) {
|
|
|
938
1010
|
// the package-wide answer for everything that has no pack in hand.
|
|
939
1011
|
systemId: derived.systemId,
|
|
940
1012
|
systemVersion: derived.systemVersion,
|
|
941
|
-
lastModifiedBy: requireNonEmptyString(
|
|
942
|
-
input.lastModifiedBy,
|
|
943
|
-
"stats.lastModifiedBy",
|
|
944
|
-
),
|
|
1013
|
+
lastModifiedBy: requireNonEmptyString(input.lastModifiedBy, "stats.lastModifiedBy"),
|
|
945
1014
|
});
|
|
946
1015
|
}
|
|
947
1016
|
|
|
@@ -1056,10 +1125,7 @@ function normalizeSectionMeta(value, where) {
|
|
|
1056
1125
|
out.banner = requireNonEmptyString(input.banner, `${where}.banner`);
|
|
1057
1126
|
}
|
|
1058
1127
|
if (input.description !== undefined) {
|
|
1059
|
-
out.description = requireNonEmptyString(
|
|
1060
|
-
input.description,
|
|
1061
|
-
`${where}.description`,
|
|
1062
|
-
);
|
|
1128
|
+
out.description = requireNonEmptyString(input.description, `${where}.description`);
|
|
1063
1129
|
}
|
|
1064
1130
|
return Object.freeze(out);
|
|
1065
1131
|
}
|
|
@@ -1075,9 +1141,7 @@ function normalizeSectionMap(value, where) {
|
|
|
1075
1141
|
if (value === undefined) return Object.freeze({});
|
|
1076
1142
|
if (!isPlainObject(value)) fail(where, "must be a mapping");
|
|
1077
1143
|
const out = {};
|
|
1078
|
-
for (const [name, meta] of Object.entries(
|
|
1079
|
-
/** @type {Record<string, unknown>} */ (value),
|
|
1080
|
-
)) {
|
|
1144
|
+
for (const [name, meta] of Object.entries(/** @type {Record<string, unknown>} */ (value))) {
|
|
1081
1145
|
out[name] = normalizeSectionMeta(meta, `${where}.${name}`);
|
|
1082
1146
|
}
|
|
1083
1147
|
return Object.freeze(out);
|
|
@@ -1124,10 +1188,7 @@ function normalizeSite(value) {
|
|
|
1124
1188
|
trees.push(
|
|
1125
1189
|
Object.freeze({
|
|
1126
1190
|
from: requireNonEmptyString(tree.from, `${where}.from`),
|
|
1127
|
-
section: requireNonEmptyString(
|
|
1128
|
-
tree.section,
|
|
1129
|
-
`${where}.section`,
|
|
1130
|
-
),
|
|
1191
|
+
section: requireNonEmptyString(tree.section, `${where}.section`),
|
|
1131
1192
|
// The tree's own path, POSIX-separated — what a
|
|
1132
1193
|
// repository-relative link inside it is resolved against.
|
|
1133
1194
|
rel: String(tree.from).split(path.sep).join("/"),
|
|
@@ -1141,9 +1202,7 @@ function normalizeSite(value) {
|
|
|
1141
1202
|
if (!Array.isArray(input.packages)) {
|
|
1142
1203
|
fail("site.packages", "must be a list");
|
|
1143
1204
|
}
|
|
1144
|
-
packages = input.packages.map((p, i) =>
|
|
1145
|
-
requireNonEmptyString(p, `site.packages[${i}]`),
|
|
1146
|
-
);
|
|
1205
|
+
packages = input.packages.map((p, i) => requireNonEmptyString(p, `site.packages[${i}]`));
|
|
1147
1206
|
}
|
|
1148
1207
|
|
|
1149
1208
|
let landing = null;
|
|
@@ -1157,35 +1216,19 @@ function normalizeSite(value) {
|
|
|
1157
1216
|
}
|
|
1158
1217
|
|
|
1159
1218
|
return Object.freeze({
|
|
1160
|
-
out:
|
|
1161
|
-
|
|
1162
|
-
""
|
|
1163
|
-
: requireNonEmptyString(input.out, "site.out"),
|
|
1164
|
-
base:
|
|
1165
|
-
input.base === undefined ?
|
|
1166
|
-
""
|
|
1167
|
-
: requireNonEmptyString(input.base, "site.base"),
|
|
1219
|
+
out: input.out === undefined ? "" : requireNonEmptyString(input.out, "site.out"),
|
|
1220
|
+
base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
|
|
1168
1221
|
packages: Object.freeze(packages),
|
|
1169
1222
|
sections: normalizeSectionMap(input.sections, "site.sections"),
|
|
1170
|
-
readmeSections: normalizeSectionMap(
|
|
1171
|
-
input.readmeSections,
|
|
1172
|
-
"site.readmeSections",
|
|
1173
|
-
),
|
|
1223
|
+
readmeSections: normalizeSectionMap(input.readmeSections, "site.readmeSections"),
|
|
1174
1224
|
landing,
|
|
1175
1225
|
trees: Object.freeze(trees),
|
|
1176
|
-
pass:
|
|
1177
|
-
input.pass === undefined ?
|
|
1178
|
-
""
|
|
1179
|
-
: requireNonEmptyString(input.pass, "site.pass"),
|
|
1226
|
+
pass: input.pass === undefined ? "" : requireNonEmptyString(input.pass, "site.pass"),
|
|
1180
1227
|
passOptions:
|
|
1181
1228
|
input.passOptions === undefined ?
|
|
1182
1229
|
Object.freeze({})
|
|
1183
1230
|
: Object.freeze({ ...input.passOptions }),
|
|
1184
|
-
backfillSections: optionalBoolean(
|
|
1185
|
-
input.backfillSections,
|
|
1186
|
-
"site.backfillSections",
|
|
1187
|
-
false,
|
|
1188
|
-
),
|
|
1231
|
+
backfillSections: optionalBoolean(input.backfillSections, "site.backfillSections", false),
|
|
1189
1232
|
});
|
|
1190
1233
|
}
|
|
1191
1234
|
|
|
@@ -1213,10 +1256,7 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1213
1256
|
out.minimum = requireNonEmptyString(input.minimum, `${where}.minimum`);
|
|
1214
1257
|
}
|
|
1215
1258
|
if (input.verified !== undefined) {
|
|
1216
|
-
out.verified = requireNonEmptyString(
|
|
1217
|
-
input.verified,
|
|
1218
|
-
`${where}.verified`,
|
|
1219
|
-
);
|
|
1259
|
+
out.verified = requireNonEmptyString(input.verified, `${where}.verified`);
|
|
1220
1260
|
}
|
|
1221
1261
|
return Object.freeze(out);
|
|
1222
1262
|
}
|
|
@@ -1255,8 +1295,7 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1255
1295
|
*/
|
|
1256
1296
|
function normalizeSystems(value) {
|
|
1257
1297
|
if (value === undefined || value === null) return Object.freeze({});
|
|
1258
|
-
if (!isPlainObject(value))
|
|
1259
|
-
fail("systems", "must be a mapping of id to spec");
|
|
1298
|
+
if (!isPlainObject(value)) fail("systems", "must be a mapping of id to spec");
|
|
1260
1299
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1261
1300
|
|
|
1262
1301
|
const out = {};
|
|
@@ -1276,10 +1315,7 @@ function normalizeSystems(value) {
|
|
|
1276
1315
|
// `verified` is required because it is the value a pack stamps. A
|
|
1277
1316
|
// declaration that cannot answer "which version was this built
|
|
1278
1317
|
// against" is the gap this block exists to close.
|
|
1279
|
-
const verified = requireNonEmptyString(
|
|
1280
|
-
compat.verified,
|
|
1281
|
-
`${at}.compatibility.verified`,
|
|
1282
|
-
);
|
|
1318
|
+
const verified = requireNonEmptyString(compat.verified, `${at}.compatibility.verified`);
|
|
1283
1319
|
|
|
1284
1320
|
out[id] = Object.freeze({
|
|
1285
1321
|
manifest:
|
|
@@ -1290,10 +1326,7 @@ function normalizeSystems(value) {
|
|
|
1290
1326
|
minimum:
|
|
1291
1327
|
compat.minimum === undefined || compat.minimum === null ?
|
|
1292
1328
|
null
|
|
1293
|
-
: requireNonEmptyString(
|
|
1294
|
-
compat.minimum,
|
|
1295
|
-
`${at}.compatibility.minimum`,
|
|
1296
|
-
),
|
|
1329
|
+
: requireNonEmptyString(compat.minimum, `${at}.compatibility.minimum`),
|
|
1297
1330
|
verified,
|
|
1298
1331
|
}),
|
|
1299
1332
|
});
|
|
@@ -1346,10 +1379,7 @@ function normalizeRelationships(value) {
|
|
|
1346
1379
|
};
|
|
1347
1380
|
for (const key of ["type", "manifest"]) {
|
|
1348
1381
|
if (rel[key] !== undefined) {
|
|
1349
|
-
spec[key] = requireNonEmptyString(
|
|
1350
|
-
rel[key],
|
|
1351
|
-
`${at}.${key}`,
|
|
1352
|
-
);
|
|
1382
|
+
spec[key] = requireNonEmptyString(rel[key], `${at}.${key}`);
|
|
1353
1383
|
}
|
|
1354
1384
|
}
|
|
1355
1385
|
const compat = normalizeCompatibility(
|
|
@@ -1367,10 +1397,7 @@ function normalizeRelationships(value) {
|
|
|
1367
1397
|
fail(`${at}.itemCatalog`, "must be true or false");
|
|
1368
1398
|
}
|
|
1369
1399
|
if (rel.itemCatalog && spec.manifest === undefined) {
|
|
1370
|
-
fail(
|
|
1371
|
-
`${at}.itemCatalog`,
|
|
1372
|
-
"needs a `manifest` naming the package to fetch",
|
|
1373
|
-
);
|
|
1400
|
+
fail(`${at}.itemCatalog`, "needs a `manifest` naming the package to fetch");
|
|
1374
1401
|
}
|
|
1375
1402
|
spec.itemCatalog = rel.itemCatalog;
|
|
1376
1403
|
}
|
|
@@ -1397,9 +1424,7 @@ function normalizePackageBuild(value) {
|
|
|
1397
1424
|
"reads, and that package validates what is inside it",
|
|
1398
1425
|
);
|
|
1399
1426
|
}
|
|
1400
|
-
return /** @type {Readonly<PackageBuildSection>} */ (
|
|
1401
|
-
deepFreeze(structuredClone(value))
|
|
1402
|
-
);
|
|
1427
|
+
return /** @type {Readonly<PackageBuildSection>} */ (deepFreeze(structuredClone(value)));
|
|
1403
1428
|
}
|
|
1404
1429
|
|
|
1405
1430
|
/**
|
|
@@ -1427,19 +1452,18 @@ function normalizePackageBuild(value) {
|
|
|
1427
1452
|
* Art now travels with the builder it belongs to, which is the one place a type
|
|
1428
1453
|
* is already declared.
|
|
1429
1454
|
*
|
|
1430
|
-
* @param {unknown} value
|
|
1431
|
-
* @
|
|
1432
|
-
*
|
|
1455
|
+
* @param {unknown} value - One registry: type → entry.
|
|
1456
|
+
* @param {string} at - The configuration path to report against.
|
|
1457
|
+
* @returns {{itemBuilders: Record<string, Function>,
|
|
1458
|
+
* itemArt: Record<string, string>,
|
|
1459
|
+
* itemFields: Record<string, readonly object[]>}}
|
|
1433
1460
|
* The `system` builder for each type, and the default art for those types
|
|
1434
1461
|
* that paired one. The art table is deliberately *sparse*: a bare-function
|
|
1435
1462
|
* entry contributes no key, which is what distinguishes "no default art" from
|
|
1436
1463
|
* an empty one.
|
|
1437
1464
|
*/
|
|
1438
|
-
function
|
|
1439
|
-
if (value
|
|
1440
|
-
return { itemBuilders: Object.freeze({}), itemArt: Object.freeze({}) };
|
|
1441
|
-
}
|
|
1442
|
-
if (!isPlainObject(value)) fail("itemBuilders", "must be an object");
|
|
1465
|
+
function normalizeOneRegistry(value, at) {
|
|
1466
|
+
if (!isPlainObject(value)) fail(at, "must be an object");
|
|
1443
1467
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1444
1468
|
|
|
1445
1469
|
/** @type {Record<string, Function>} */
|
|
@@ -1456,46 +1480,167 @@ function normalizeItemBuilders(value) {
|
|
|
1456
1480
|
}
|
|
1457
1481
|
if (!isPlainObject(entry)) {
|
|
1458
1482
|
fail(
|
|
1459
|
-
|
|
1483
|
+
`${at}.${type}`,
|
|
1460
1484
|
"must be a builder function, or an object with a `system` builder",
|
|
1461
1485
|
);
|
|
1462
1486
|
}
|
|
1463
1487
|
const paired = /** @type {Record<string, unknown>} */ (entry);
|
|
1464
|
-
rejectUnknownKeys(paired, ITEM_BUILDER_KEYS,
|
|
1488
|
+
rejectUnknownKeys(paired, ITEM_BUILDER_KEYS, `${at}.${type}.`);
|
|
1465
1489
|
if (typeof paired.system !== "function") {
|
|
1466
|
-
fail(
|
|
1490
|
+
fail(`${at}.${type}.system`, "must be a function");
|
|
1467
1491
|
}
|
|
1468
1492
|
itemBuilders[type] = /** @type {Function} */ (paired.system);
|
|
1469
1493
|
if (paired.img !== undefined) {
|
|
1470
|
-
itemArt[type] = requireNonEmptyString(
|
|
1471
|
-
paired.img,
|
|
1472
|
-
`itemBuilders.${type}.img`,
|
|
1473
|
-
);
|
|
1494
|
+
itemArt[type] = requireNonEmptyString(paired.img, `${at}.${type}.img`);
|
|
1474
1495
|
}
|
|
1475
1496
|
if (paired.fields !== undefined) {
|
|
1476
1497
|
if (!Array.isArray(paired.fields)) {
|
|
1477
|
-
fail(
|
|
1498
|
+
fail(`${at}.${type}.fields`, "must be an array");
|
|
1478
1499
|
}
|
|
1479
1500
|
for (const [index, field] of paired.fields.entries()) {
|
|
1480
1501
|
if (!isPlainObject(field)) {
|
|
1481
|
-
fail(
|
|
1482
|
-
`itemBuilders.${type}.fields[${index}]`,
|
|
1483
|
-
"must be a field declaration object",
|
|
1484
|
-
);
|
|
1502
|
+
fail(`${at}.${type}.fields[${index}]`, "must be a field declaration object");
|
|
1485
1503
|
}
|
|
1486
1504
|
requireNonEmptyString(
|
|
1487
1505
|
/** @type {Record<string, unknown>} */ (field).to,
|
|
1488
|
-
|
|
1506
|
+
`${at}.${type}.fields[${index}].to`,
|
|
1489
1507
|
);
|
|
1490
1508
|
}
|
|
1491
1509
|
itemFields[type] = Object.freeze([...paired.fields]);
|
|
1492
1510
|
}
|
|
1493
1511
|
}
|
|
1494
1512
|
|
|
1513
|
+
return { itemBuilders, itemArt, itemFields };
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* The declared item-builder registries, and the vocabulary their union gives
|
|
1518
|
+
* (#58).
|
|
1519
|
+
*
|
|
1520
|
+
* **One registry is a ceiling, not a default.** The accepted type list is the
|
|
1521
|
+
* registry's keys, which is what makes a type impossible to accept without a
|
|
1522
|
+
* builder behind it (#1504) — and, with one registry, impossible to accept a
|
|
1523
|
+
* type a *second* system declares. A tree feeding two systems has both:
|
|
1524
|
+
* `spell`, `invocation` and `psionic` are HM3's, `mysticalability` and
|
|
1525
|
+
* `projectilegear` are SoHL's, and `skill` is both systems' under one name and
|
|
1526
|
+
* two data models.
|
|
1527
|
+
*
|
|
1528
|
+
* So `itemBuilders` accepts either form:
|
|
1529
|
+
*
|
|
1530
|
+
* - **A registry** — `{ skill: fn, … }`. Unchanged, and what every existing
|
|
1531
|
+
* configuration declares. It names no system, because there is only one.
|
|
1532
|
+
* - **A list of registries** — `[{ system: "sohl", builders: {…} }, …]`. The
|
|
1533
|
+
* vocabulary is the **union** of their keys; a type more than one declares
|
|
1534
|
+
* keeps a builder per system, so nothing is chosen for the build silently.
|
|
1535
|
+
*
|
|
1536
|
+
* The **flat** tables — `itemBuilders`, `itemArt`, `itemFields` — are the union
|
|
1537
|
+
* with the first declaring registry winning a collision. They answer a
|
|
1538
|
+
* single-system build, where a collision cannot arise; a build with two systems
|
|
1539
|
+
* asks by system, and `itemTypesBySeveralSystems` names the types where asking
|
|
1540
|
+
* flatly would be answering the wrong question. See `engine/item-registry.mjs`,
|
|
1541
|
+
* which refuses exactly those without a system.
|
|
1542
|
+
*
|
|
1543
|
+
* @param {unknown} value - The declared `itemBuilders`.
|
|
1544
|
+
* @returns {{itemBuilders: Readonly<Record<string, Function>>,
|
|
1545
|
+
* itemArt: Readonly<Record<string, string>>,
|
|
1546
|
+
* itemFields: Readonly<Record<string, readonly object[]>>,
|
|
1547
|
+
* itemBuildersBySystem: Readonly<Record<string, Readonly<Record<string, Function>>>>,
|
|
1548
|
+
* itemArtBySystem: Readonly<Record<string, Readonly<Record<string, string>>>>,
|
|
1549
|
+
* itemFieldsBySystem: Readonly<Record<string, Readonly<Record<string, readonly object[]>>>>,
|
|
1550
|
+
* itemTypesBySeveralSystems: ReadonlySet<string>}}
|
|
1551
|
+
* The flat tables, the per-system ones, and the contested types.
|
|
1552
|
+
*/
|
|
1553
|
+
function normalizeItemBuilders(value) {
|
|
1554
|
+
const empty = Object.freeze({});
|
|
1555
|
+
if (value === undefined) {
|
|
1556
|
+
return {
|
|
1557
|
+
itemBuilders: empty,
|
|
1558
|
+
itemArt: empty,
|
|
1559
|
+
itemFields: empty,
|
|
1560
|
+
itemBuildersBySystem: empty,
|
|
1561
|
+
itemArtBySystem: empty,
|
|
1562
|
+
itemFieldsBySystem: empty,
|
|
1563
|
+
itemTypesBySeveralSystems: Object.freeze(new Set()),
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** @type {{system: string|null, tables: ReturnType<typeof normalizeOneRegistry>}[]} */
|
|
1568
|
+
const registries = [];
|
|
1569
|
+
|
|
1570
|
+
if (Array.isArray(value)) {
|
|
1571
|
+
const seen = new Set();
|
|
1572
|
+
for (const [index, entry] of value.entries()) {
|
|
1573
|
+
const at = `itemBuilders[${index}]`;
|
|
1574
|
+
if (!isPlainObject(entry)) {
|
|
1575
|
+
fail(
|
|
1576
|
+
at,
|
|
1577
|
+
"must be `{ system, builders }` — a registry and the system it belongs to",
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
const declared = /** @type {Record<string, unknown>} */ (entry);
|
|
1581
|
+
rejectUnknownKeys(declared, ITEM_REGISTRY_KEYS, `${at}.`);
|
|
1582
|
+
const system = requireNonEmptyString(declared.system, `${at}.system`);
|
|
1583
|
+
if (seen.has(system)) {
|
|
1584
|
+
fail(
|
|
1585
|
+
at,
|
|
1586
|
+
`declares a second registry for \`${system}\` — a system has one ` +
|
|
1587
|
+
`item vocabulary, so merge them at their source`,
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
seen.add(system);
|
|
1591
|
+
registries.push({
|
|
1592
|
+
system,
|
|
1593
|
+
tables: normalizeOneRegistry(declared.builders, `${at}.builders`),
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
} else {
|
|
1597
|
+
registries.push({ system: null, tables: normalizeOneRegistry(value, "itemBuilders") });
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/** @type {Record<string, Function>} */
|
|
1601
|
+
const itemBuilders = {};
|
|
1602
|
+
/** @type {Record<string, string>} */
|
|
1603
|
+
const itemArt = {};
|
|
1604
|
+
/** @type {Record<string, readonly object[]>} */
|
|
1605
|
+
const itemFields = {};
|
|
1606
|
+
/** @type {Record<string, Readonly<Record<string, Function>>>} */
|
|
1607
|
+
const itemBuildersBySystem = {};
|
|
1608
|
+
/** @type {Record<string, Readonly<Record<string, string>>>} */
|
|
1609
|
+
const itemArtBySystem = {};
|
|
1610
|
+
/** @type {Record<string, Readonly<Record<string, readonly object[]>>>} */
|
|
1611
|
+
const itemFieldsBySystem = {};
|
|
1612
|
+
/** @type {Map<string, number>} */
|
|
1613
|
+
const declaringSystems = new Map();
|
|
1614
|
+
|
|
1615
|
+
for (const { system, tables } of registries) {
|
|
1616
|
+
for (const [type, builder] of Object.entries(tables.itemBuilders)) {
|
|
1617
|
+
declaringSystems.set(type, (declaringSystems.get(type) ?? 0) + 1);
|
|
1618
|
+
if (!(type in itemBuilders)) itemBuilders[type] = builder;
|
|
1619
|
+
}
|
|
1620
|
+
for (const [type, art] of Object.entries(tables.itemArt)) {
|
|
1621
|
+
if (!(type in itemArt)) itemArt[type] = art;
|
|
1622
|
+
}
|
|
1623
|
+
for (const [type, fields] of Object.entries(tables.itemFields)) {
|
|
1624
|
+
if (!(type in itemFields)) itemFields[type] = fields;
|
|
1625
|
+
}
|
|
1626
|
+
if (system === null) continue;
|
|
1627
|
+
itemBuildersBySystem[system] = Object.freeze(tables.itemBuilders);
|
|
1628
|
+
itemArtBySystem[system] = Object.freeze(tables.itemArt);
|
|
1629
|
+
itemFieldsBySystem[system] = Object.freeze(tables.itemFields);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1495
1632
|
return {
|
|
1496
1633
|
itemBuilders: Object.freeze(itemBuilders),
|
|
1497
1634
|
itemArt: Object.freeze(itemArt),
|
|
1498
1635
|
itemFields: Object.freeze(itemFields),
|
|
1636
|
+
itemBuildersBySystem: Object.freeze(itemBuildersBySystem),
|
|
1637
|
+
itemArtBySystem: Object.freeze(itemArtBySystem),
|
|
1638
|
+
itemFieldsBySystem: Object.freeze(itemFieldsBySystem),
|
|
1639
|
+
itemTypesBySeveralSystems: Object.freeze(
|
|
1640
|
+
new Set(
|
|
1641
|
+
[...declaringSystems.entries()].filter(([, count]) => count > 1).map(([t]) => t),
|
|
1642
|
+
),
|
|
1643
|
+
),
|
|
1499
1644
|
};
|
|
1500
1645
|
}
|
|
1501
1646
|
|
|
@@ -1556,9 +1701,7 @@ function normalizePublish(value) {
|
|
|
1556
1701
|
if (manifestsInput !== undefined && !isPlainObject(manifestsInput)) {
|
|
1557
1702
|
fail("publish.manifests", "must be an object");
|
|
1558
1703
|
}
|
|
1559
|
-
const manifests = /** @type {Record<string, unknown>} */ (
|
|
1560
|
-
manifestsInput ?? {}
|
|
1561
|
-
);
|
|
1704
|
+
const manifests = /** @type {Record<string, unknown>} */ (manifestsInput ?? {});
|
|
1562
1705
|
rejectUnknownKeys(manifests, MANIFEST_KEYS, "publish.manifests.");
|
|
1563
1706
|
|
|
1564
1707
|
const addressInput = publish.address;
|
|
@@ -1589,26 +1732,15 @@ function normalizePublish(value) {
|
|
|
1589
1732
|
DEFAULT_ADDRESS_SCHEME.landing
|
|
1590
1733
|
: optionalString(address.landing, "publish.address.landing");
|
|
1591
1734
|
if (!LANDING_RULES.includes(landing)) {
|
|
1592
|
-
fail(
|
|
1593
|
-
"publish.address.landing",
|
|
1594
|
-
`must be one of ${LANDING_RULES.join(", ")}`,
|
|
1595
|
-
);
|
|
1735
|
+
fail("publish.address.landing", `must be one of ${LANDING_RULES.join(", ")}`);
|
|
1596
1736
|
}
|
|
1597
1737
|
|
|
1598
1738
|
return Object.freeze({
|
|
1599
1739
|
site: normalizeSiteMode(publish.site),
|
|
1600
1740
|
address: Object.freeze({ prefix, landing }),
|
|
1601
1741
|
manifests: Object.freeze({
|
|
1602
|
-
publish: optionalBoolean(
|
|
1603
|
-
|
|
1604
|
-
"publish.manifests.publish",
|
|
1605
|
-
false,
|
|
1606
|
-
),
|
|
1607
|
-
consume: optionalBoolean(
|
|
1608
|
-
manifests.consume,
|
|
1609
|
-
"publish.manifests.consume",
|
|
1610
|
-
false,
|
|
1611
|
-
),
|
|
1742
|
+
publish: optionalBoolean(manifests.publish, "publish.manifests.publish", false),
|
|
1743
|
+
consume: optionalBoolean(manifests.consume, "publish.manifests.consume", false),
|
|
1612
1744
|
}),
|
|
1613
1745
|
});
|
|
1614
1746
|
}
|
|
@@ -1629,13 +1761,9 @@ function normalizePublish(value) {
|
|
|
1629
1761
|
*/
|
|
1630
1762
|
export function defineConfig(config) {
|
|
1631
1763
|
if (!isPlainObject(config)) {
|
|
1632
|
-
throw new TypeError(
|
|
1633
|
-
"package-build config: expected a configuration object.",
|
|
1634
|
-
);
|
|
1764
|
+
throw new TypeError("package-build config: expected a configuration object.");
|
|
1635
1765
|
}
|
|
1636
|
-
const input = /** @type {Record<string, unknown>} */ (
|
|
1637
|
-
/** @type {unknown} */ (config)
|
|
1638
|
-
);
|
|
1766
|
+
const input = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (config));
|
|
1639
1767
|
rejectUnknownKeys(input, CONFIG_KEYS, "");
|
|
1640
1768
|
|
|
1641
1769
|
const rootDir = requireNonEmptyString(input.rootDir, "rootDir");
|
|
@@ -1650,21 +1778,14 @@ export function defineConfig(config) {
|
|
|
1650
1778
|
const packageKind = input.packageKind;
|
|
1651
1779
|
if (
|
|
1652
1780
|
typeof packageKind !== "string" ||
|
|
1653
|
-
!(
|
|
1654
|
-
/** @type {readonly string[]} */ (PACKAGE_KINDS).includes(
|
|
1655
|
-
packageKind,
|
|
1656
|
-
)
|
|
1657
|
-
)
|
|
1781
|
+
!(/** @type {readonly string[]} */ (PACKAGE_KINDS).includes(packageKind))
|
|
1658
1782
|
) {
|
|
1659
1783
|
fail("packageKind", `must be one of: ${PACKAGE_KINDS.join(", ")}`);
|
|
1660
1784
|
}
|
|
1661
1785
|
|
|
1662
1786
|
if (!Array.isArray(input.packs)) fail("packs", "must be an array");
|
|
1663
|
-
if (input.packs.length === 0)
|
|
1664
|
-
|
|
1665
|
-
const packs = input.packs.map((pack, index) =>
|
|
1666
|
-
normalizePack(pack, `packs[${index}]`),
|
|
1667
|
-
);
|
|
1787
|
+
if (input.packs.length === 0) fail("packs", "must declare at least one pack");
|
|
1788
|
+
const packs = input.packs.map((pack, index) => normalizePack(pack, `packs[${index}]`));
|
|
1668
1789
|
|
|
1669
1790
|
// One list, so the compile order and the directory list cannot disagree —
|
|
1670
1791
|
// they used to be `PACK_CONFIGS` and `SOURCE_PACKS`, maintained apart (#1508).
|
|
@@ -1686,9 +1807,7 @@ export function defineConfig(config) {
|
|
|
1686
1807
|
const declaredSystems = new Set(Object.keys(systems));
|
|
1687
1808
|
/** `relationships.systems`, for the derivations that still consult it. */
|
|
1688
1809
|
const relationshipSystems = /** @type {{id?: string}[]} */ (
|
|
1689
|
-
(isPlainObject(input.relationships) ?
|
|
1690
|
-
input.relationships.systems
|
|
1691
|
-
: null) ?? []
|
|
1810
|
+
(isPlainObject(input.relationships) ? input.relationships.systems : null) ?? []
|
|
1692
1811
|
);
|
|
1693
1812
|
|
|
1694
1813
|
// A name that resolves to nothing is a build error rather than a
|
|
@@ -1747,32 +1866,33 @@ export function defineConfig(config) {
|
|
|
1747
1866
|
defaultsByType.set(pack.type, pack.name);
|
|
1748
1867
|
}
|
|
1749
1868
|
|
|
1750
|
-
if (
|
|
1751
|
-
input.skipDirectories !== undefined &&
|
|
1752
|
-
!Array.isArray(input.skipDirectories)
|
|
1753
|
-
) {
|
|
1869
|
+
if (input.skipDirectories !== undefined && !Array.isArray(input.skipDirectories)) {
|
|
1754
1870
|
fail("skipDirectories", "must be an array");
|
|
1755
1871
|
}
|
|
1756
1872
|
const skipDirectories = (input.skipDirectories ?? []).map((name, index) =>
|
|
1757
1873
|
requireNonEmptyString(name, `skipDirectories[${index}]`),
|
|
1758
1874
|
);
|
|
1759
1875
|
|
|
1760
|
-
const foundryPackage = requireNonEmptyString(
|
|
1761
|
-
input.foundryPackage,
|
|
1762
|
-
"foundryPackage",
|
|
1763
|
-
);
|
|
1876
|
+
const foundryPackage = requireNonEmptyString(input.foundryPackage, "foundryPackage");
|
|
1764
1877
|
|
|
1765
|
-
const {
|
|
1766
|
-
|
|
1767
|
-
|
|
1878
|
+
const {
|
|
1879
|
+
itemBuilders,
|
|
1880
|
+
itemArt,
|
|
1881
|
+
itemFields,
|
|
1882
|
+
itemBuildersBySystem,
|
|
1883
|
+
itemArtBySystem,
|
|
1884
|
+
itemFieldsBySystem,
|
|
1885
|
+
itemTypesBySeveralSystems,
|
|
1886
|
+
} = normalizeItemBuilders(input.itemBuilders);
|
|
1887
|
+
// The union across every declared registry (#58) — the flat table already
|
|
1888
|
+
// holds every key any of them declares, so this stays "the registry's keys"
|
|
1889
|
+
// rather than becoming a second list to keep in step (#1504).
|
|
1768
1890
|
const itemTypes = Object.freeze(new Set(Object.keys(itemBuilders)));
|
|
1891
|
+
const docEntryTypes = Object.freeze(new Set([...itemTypes, "macro", ...MAP_TYPES]));
|
|
1769
1892
|
|
|
1770
1893
|
return Object.freeze({
|
|
1771
1894
|
rootDir,
|
|
1772
|
-
contentPackage:
|
|
1773
|
-
input.contentPackage,
|
|
1774
|
-
"contentPackage",
|
|
1775
|
-
),
|
|
1895
|
+
contentPackage: requireContentPackage(input.contentPackage, docEntryTypes),
|
|
1776
1896
|
foundryPackage,
|
|
1777
1897
|
packageKind: /** @type {PackageKind} */ (packageKind),
|
|
1778
1898
|
// Foundry serves a package's files from `<kind>/<id>/`, so this is the
|
|
@@ -1798,9 +1918,8 @@ export function defineConfig(config) {
|
|
|
1798
1918
|
// which is the duplication this whole change exists to
|
|
1799
1919
|
// remove. Several entries have no single answer and get
|
|
1800
1920
|
// none.
|
|
1801
|
-
: relationshipSystems.length === 1 ?
|
|
1802
|
-
|
|
1803
|
-
: null,
|
|
1921
|
+
: relationshipSystems.length === 1 ? (relationshipSystems[0]?.id ?? null)
|
|
1922
|
+
: null,
|
|
1804
1923
|
// Derived here where the answer is pure data — the `verified` of
|
|
1805
1924
|
// whichever system the package-wide block takes — and supplied by
|
|
1806
1925
|
// the loader otherwise. The loader is the half that may do I/O, and
|
|
@@ -1811,21 +1930,19 @@ export function defineConfig(config) {
|
|
|
1811
1930
|
(() => {
|
|
1812
1931
|
const id =
|
|
1813
1932
|
requiresSystem ??
|
|
1814
|
-
(Object.keys(systems).length === 1 ?
|
|
1815
|
-
|
|
1816
|
-
: null);
|
|
1817
|
-
return id ?
|
|
1818
|
-
(systems[id]?.compatibility?.verified ?? null)
|
|
1819
|
-
: null;
|
|
1933
|
+
(Object.keys(systems).length === 1 ? Object.keys(systems)[0] : null);
|
|
1934
|
+
return id ? (systems[id]?.compatibility?.verified ?? null) : null;
|
|
1820
1935
|
})() ??
|
|
1821
|
-
(isPlainObject(input.stats) ?
|
|
1822
|
-
input.stats[DERIVED_SYSTEM_VERSION]
|
|
1823
|
-
: null) ??
|
|
1936
|
+
(isPlainObject(input.stats) ? input.stats[DERIVED_SYSTEM_VERSION] : null) ??
|
|
1824
1937
|
null,
|
|
1825
1938
|
}),
|
|
1826
1939
|
itemBuilders,
|
|
1827
1940
|
itemArt,
|
|
1828
1941
|
itemFields,
|
|
1942
|
+
itemBuildersBySystem,
|
|
1943
|
+
itemArtBySystem,
|
|
1944
|
+
itemFieldsBySystem,
|
|
1945
|
+
itemTypesBySeveralSystems,
|
|
1829
1946
|
// Resolved once, here, and read everywhere through
|
|
1830
1947
|
// `loadPackConfig()`. The doc-entry *concept* is the engine's —
|
|
1831
1948
|
// a note that carries documentation is not a SoHL idea — but the
|
|
@@ -1833,18 +1950,13 @@ export function defineConfig(config) {
|
|
|
1833
1950
|
// runtime. Two would drift, which is the whole reason the composition
|
|
1834
1951
|
// was written down in one place to begin with.
|
|
1835
1952
|
itemTypes,
|
|
1836
|
-
docEntryTypes
|
|
1837
|
-
new Set([...itemTypes, "macro", ...MAP_TYPES]),
|
|
1838
|
-
),
|
|
1953
|
+
docEntryTypes,
|
|
1839
1954
|
skipDirectories: Object.freeze(skipDirectories),
|
|
1840
1955
|
packs: Object.freeze(packs),
|
|
1841
1956
|
packDirectories: Object.freeze(packDirectories),
|
|
1842
1957
|
docs: normalizeDocs(input.docs),
|
|
1843
1958
|
site: normalizeSite(input.site),
|
|
1844
|
-
compatibility: normalizeCompatibility(
|
|
1845
|
-
input.compatibility,
|
|
1846
|
-
"compatibility",
|
|
1847
|
-
),
|
|
1959
|
+
compatibility: normalizeCompatibility(input.compatibility, "compatibility"),
|
|
1848
1960
|
relationships: normalizeRelationships(input.relationships),
|
|
1849
1961
|
systems,
|
|
1850
1962
|
requiresSystem,
|