@heroiclands/package-build 9.0.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 +694 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +437 -7
- package/content-config.mjs +259 -28
- package/docs/content-format.md +1418 -0
- package/engine/address-charset.mjs +62 -0
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +194 -4
- package/engine/content-address.mjs +4 -4
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +132 -56
- package/engine/content-lint.mjs +8 -1
- package/engine/diagnostics.mjs +33 -0
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-spec.mjs +49 -43
- package/engine/frontmatter-lint.mjs +343 -27
- package/engine/generate.mjs +32 -4
- package/engine/helpers.mjs +41 -29
- package/engine/ids.mjs +19 -1
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -5
- package/engine/kb-manifest.mjs +36 -7
- package/engine/map-notes.mjs +34 -18
- package/engine/note-claims.mjs +383 -0
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +39 -22
- package/engine/pack-router.mjs +17 -6
- package/engine/prose-lint.mjs +55 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scenes.mjs +19 -1
- package/engine/schema-check.mjs +347 -3
- package/engine/site-build.mjs +1 -1
- package/engine/site-index.mjs +38 -21
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +112 -80
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +67 -51
- package/package.json +6 -2
- package/sohl/actors.mjs +249 -36
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +3 -0
- package/sohl/items.mjs +110 -14
- package/sohl/note-schemas.mjs +11 -7
- package/types/content-config.d.mts +48 -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 +14 -1
- package/types/engine/helpers.d.mts +21 -13
- 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/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/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
|
|
@@ -610,6 +642,7 @@ const DOC_PAGE_KEYS = ["title", "out", "preamble"];
|
|
|
610
642
|
const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
|
|
611
643
|
const RELATIONSHIP_KEYS = ["id", "type", "manifest", "compatibility", "itemCatalog"];
|
|
612
644
|
const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
|
|
645
|
+
const ITEM_REGISTRY_KEYS = ["system", "builders"];
|
|
613
646
|
const PACK_KEYS = [
|
|
614
647
|
"name",
|
|
615
648
|
"type",
|
|
@@ -695,6 +728,65 @@ function requireNonEmptyString(value, field) {
|
|
|
695
728
|
return /** @type {string} */ (value);
|
|
696
729
|
}
|
|
697
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
|
+
|
|
698
790
|
/**
|
|
699
791
|
* @param {unknown} value
|
|
700
792
|
* @param {string} field
|
|
@@ -1360,19 +1452,18 @@ function normalizePackageBuild(value) {
|
|
|
1360
1452
|
* Art now travels with the builder it belongs to, which is the one place a type
|
|
1361
1453
|
* is already declared.
|
|
1362
1454
|
*
|
|
1363
|
-
* @param {unknown} value
|
|
1364
|
-
* @
|
|
1365
|
-
*
|
|
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[]>}}
|
|
1366
1460
|
* The `system` builder for each type, and the default art for those types
|
|
1367
1461
|
* that paired one. The art table is deliberately *sparse*: a bare-function
|
|
1368
1462
|
* entry contributes no key, which is what distinguishes "no default art" from
|
|
1369
1463
|
* an empty one.
|
|
1370
1464
|
*/
|
|
1371
|
-
function
|
|
1372
|
-
if (value
|
|
1373
|
-
return { itemBuilders: Object.freeze({}), itemArt: Object.freeze({}) };
|
|
1374
|
-
}
|
|
1375
|
-
if (!isPlainObject(value)) fail("itemBuilders", "must be an object");
|
|
1465
|
+
function normalizeOneRegistry(value, at) {
|
|
1466
|
+
if (!isPlainObject(value)) fail(at, "must be an object");
|
|
1376
1467
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1377
1468
|
|
|
1378
1469
|
/** @type {Record<string, Function>} */
|
|
@@ -1389,43 +1480,167 @@ function normalizeItemBuilders(value) {
|
|
|
1389
1480
|
}
|
|
1390
1481
|
if (!isPlainObject(entry)) {
|
|
1391
1482
|
fail(
|
|
1392
|
-
|
|
1483
|
+
`${at}.${type}`,
|
|
1393
1484
|
"must be a builder function, or an object with a `system` builder",
|
|
1394
1485
|
);
|
|
1395
1486
|
}
|
|
1396
1487
|
const paired = /** @type {Record<string, unknown>} */ (entry);
|
|
1397
|
-
rejectUnknownKeys(paired, ITEM_BUILDER_KEYS,
|
|
1488
|
+
rejectUnknownKeys(paired, ITEM_BUILDER_KEYS, `${at}.${type}.`);
|
|
1398
1489
|
if (typeof paired.system !== "function") {
|
|
1399
|
-
fail(
|
|
1490
|
+
fail(`${at}.${type}.system`, "must be a function");
|
|
1400
1491
|
}
|
|
1401
1492
|
itemBuilders[type] = /** @type {Function} */ (paired.system);
|
|
1402
1493
|
if (paired.img !== undefined) {
|
|
1403
|
-
itemArt[type] = requireNonEmptyString(paired.img,
|
|
1494
|
+
itemArt[type] = requireNonEmptyString(paired.img, `${at}.${type}.img`);
|
|
1404
1495
|
}
|
|
1405
1496
|
if (paired.fields !== undefined) {
|
|
1406
1497
|
if (!Array.isArray(paired.fields)) {
|
|
1407
|
-
fail(
|
|
1498
|
+
fail(`${at}.${type}.fields`, "must be an array");
|
|
1408
1499
|
}
|
|
1409
1500
|
for (const [index, field] of paired.fields.entries()) {
|
|
1410
1501
|
if (!isPlainObject(field)) {
|
|
1411
|
-
fail(
|
|
1412
|
-
`itemBuilders.${type}.fields[${index}]`,
|
|
1413
|
-
"must be a field declaration object",
|
|
1414
|
-
);
|
|
1502
|
+
fail(`${at}.${type}.fields[${index}]`, "must be a field declaration object");
|
|
1415
1503
|
}
|
|
1416
1504
|
requireNonEmptyString(
|
|
1417
1505
|
/** @type {Record<string, unknown>} */ (field).to,
|
|
1418
|
-
|
|
1506
|
+
`${at}.${type}.fields[${index}].to`,
|
|
1419
1507
|
);
|
|
1420
1508
|
}
|
|
1421
1509
|
itemFields[type] = Object.freeze([...paired.fields]);
|
|
1422
1510
|
}
|
|
1423
1511
|
}
|
|
1424
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
|
+
|
|
1425
1632
|
return {
|
|
1426
1633
|
itemBuilders: Object.freeze(itemBuilders),
|
|
1427
1634
|
itemArt: Object.freeze(itemArt),
|
|
1428
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
|
+
),
|
|
1429
1644
|
};
|
|
1430
1645
|
}
|
|
1431
1646
|
|
|
@@ -1660,12 +1875,24 @@ export function defineConfig(config) {
|
|
|
1660
1875
|
|
|
1661
1876
|
const foundryPackage = requireNonEmptyString(input.foundryPackage, "foundryPackage");
|
|
1662
1877
|
|
|
1663
|
-
const {
|
|
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).
|
|
1664
1890
|
const itemTypes = Object.freeze(new Set(Object.keys(itemBuilders)));
|
|
1891
|
+
const docEntryTypes = Object.freeze(new Set([...itemTypes, "macro", ...MAP_TYPES]));
|
|
1665
1892
|
|
|
1666
1893
|
return Object.freeze({
|
|
1667
1894
|
rootDir,
|
|
1668
|
-
contentPackage:
|
|
1895
|
+
contentPackage: requireContentPackage(input.contentPackage, docEntryTypes),
|
|
1669
1896
|
foundryPackage,
|
|
1670
1897
|
packageKind: /** @type {PackageKind} */ (packageKind),
|
|
1671
1898
|
// Foundry serves a package's files from `<kind>/<id>/`, so this is the
|
|
@@ -1712,6 +1939,10 @@ export function defineConfig(config) {
|
|
|
1712
1939
|
itemBuilders,
|
|
1713
1940
|
itemArt,
|
|
1714
1941
|
itemFields,
|
|
1942
|
+
itemBuildersBySystem,
|
|
1943
|
+
itemArtBySystem,
|
|
1944
|
+
itemFieldsBySystem,
|
|
1945
|
+
itemTypesBySeveralSystems,
|
|
1715
1946
|
// Resolved once, here, and read everywhere through
|
|
1716
1947
|
// `loadPackConfig()`. The doc-entry *concept* is the engine's —
|
|
1717
1948
|
// a note that carries documentation is not a SoHL idea — but the
|
|
@@ -1719,7 +1950,7 @@ export function defineConfig(config) {
|
|
|
1719
1950
|
// runtime. Two would drift, which is the whole reason the composition
|
|
1720
1951
|
// was written down in one place to begin with.
|
|
1721
1952
|
itemTypes,
|
|
1722
|
-
docEntryTypes
|
|
1953
|
+
docEntryTypes,
|
|
1723
1954
|
skipDirectories: Object.freeze(skipDirectories),
|
|
1724
1955
|
packs: Object.freeze(packs),
|
|
1725
1956
|
packDirectories: Object.freeze(packDirectories),
|