@heroiclands/package-build 18.2.0 → 20.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 +692 -0
- package/CONTENT.md +81 -10
- package/bin/content-build.mjs +7 -1
- package/ci/ci-docker.mjs +21 -0
- package/content-config.mjs +26 -24
- package/docs/content-format.md +408 -85
- package/engine/actor-compiler.mjs +197 -7
- package/engine/address-charset.mjs +23 -5
- package/engine/base-compiler.mjs +65 -2
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +92 -1
- package/engine/content-format.mjs +102 -0
- package/engine/content-index.mjs +11 -8
- package/engine/content-links.mjs +37 -21
- package/engine/field-reference.mjs +57 -5
- package/engine/field-spec.mjs +214 -7
- package/engine/folder-notes.mjs +88 -1
- package/engine/foreign-catalog.mjs +4 -1
- package/engine/foundry-entries.mjs +16 -0
- package/engine/frontmatter-lint.mjs +215 -28
- package/engine/frontmatter.mjs +12 -12
- package/engine/generate.mjs +78 -46
- package/engine/helpers.mjs +87 -128
- package/engine/index.mjs +3 -0
- package/engine/item-compiler.mjs +44 -9
- package/engine/journals.mjs +27 -16
- package/engine/macros.mjs +8 -0
- package/engine/map-notes.mjs +7 -7
- package/engine/note-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- package/engine/retired-fields.mjs +57 -16
- package/engine/runtime-only-fields.mjs +204 -0
- package/engine/scenes.mjs +19 -28
- package/engine/schema-check.mjs +23 -1
- package/engine/site-index.mjs +17 -0
- package/engine/subtype-registry.mjs +30 -0
- package/engine/system-block.mjs +81 -3
- package/engine/web-wikilinks.mjs +33 -27
- package/engine/wikilink-syntax.mjs +7 -0
- package/engine/wikilinks.mjs +74 -16
- package/hm3/actors.mjs +70 -23
- package/package.json +2 -2
- package/sohl/actors.mjs +106 -7
- package/sohl/item-fields.mjs +203 -0
- package/sohl/note-schemas.mjs +6 -3
- package/types/content-config.d.mts +0 -7
- package/types/engine/actor-compiler.d.mts +83 -3
- package/types/engine/address-charset.d.mts +22 -4
- package/types/engine/base-compiler.d.mts +54 -3
- package/types/engine/content-address.d.mts +64 -0
- package/types/engine/content-format.d.mts +9 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +59 -0
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +18 -2
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +37 -38
- package/types/engine/index.d.mts +1 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-ids.d.mts +14 -0
- package/types/engine/retired-fields.d.mts +29 -13
- package/types/engine/runtime-only-fields.d.mts +102 -0
- package/types/engine/schema-check.d.mts +10 -1
- package/types/engine/subtype-registry.d.mts +21 -0
- package/types/engine/system-block.d.mts +28 -2
- package/types/sohl/actors.d.mts +3 -3
package/CONTENT.md
CHANGED
|
@@ -85,7 +85,7 @@ paths:
|
|
|
85
85
|
# order is worked out separately, from what each pass reads (see "Declaration
|
|
86
86
|
# order is presentation" below).
|
|
87
87
|
packs:
|
|
88
|
-
- { name: items, type: Item, label: Items
|
|
88
|
+
- { name: items, type: Item, label: Items }
|
|
89
89
|
- { name: journals, type: JournalEntry, label: Journals }
|
|
90
90
|
# A companion is written by its parent's pass rather than one of its own.
|
|
91
91
|
- name: scenes
|
|
@@ -96,7 +96,7 @@ packs:
|
|
|
96
96
|
# A pack whose per-document JSON is already built — checked in rather than
|
|
97
97
|
# generated. `prebuilt` names where it lives, generation is skipped for it,
|
|
98
98
|
# and `cleanPackEntry` and the Scene/Level integrity check still run. It may
|
|
99
|
-
# not carry `
|
|
99
|
+
# not carry `companions` or `default`, and may not be a companion:
|
|
100
100
|
# each of those describes a generation pass a prebuilt pack does not have.
|
|
101
101
|
# When every configured pack is prebuilt the content walk is skipped
|
|
102
102
|
# entirely, so a package with no `assets/content` builds.
|
|
@@ -388,6 +388,24 @@ exactly once, here, and read through `loadPackConfig()` everywhere. There is one
|
|
|
388
388
|
resolved set at runtime; the compilers and the link-manifest emitter cannot come
|
|
389
389
|
to disagree about which notes carry documentation.
|
|
390
390
|
|
|
391
|
+
**Every note compiling into a system-bearing document carries documentation, and
|
|
392
|
+
that includes actors** (#337). `docEntryTypes` is `itemTypes` plus the actor
|
|
393
|
+
types the shipped subtype maps declare (`ACTOR_TYPES`, derived from them rather
|
|
394
|
+
than listed again), plus `macro` and the map types. Only `doc` is outside it, for
|
|
395
|
+
the reason that actually applies to it: its single document _is_ the prose.
|
|
396
|
+
|
|
397
|
+
A being therefore has the same two addresses an item has — `<pkg>-<system>-being-<shortcode>`
|
|
398
|
+
for the Actor and `<pkg>-none-docbeing-<shortcode>` for the page — where before
|
|
399
|
+
it had only the first, and was the one system-bearing note a prose link could not
|
|
400
|
+
name.
|
|
401
|
+
|
|
402
|
+
**An actor keeps its prose inline as well, and that asymmetry is deliberate.** An
|
|
403
|
+
item's description is an `@UUID` pointer into its journal, because one item is
|
|
404
|
+
embedded across hundreds of beings and baking long prose into every copy bloats
|
|
405
|
+
the compendium by the length of the text times the number of carriers. An actor
|
|
406
|
+
is singular, so the same indirection costs a reader a click and saves nothing:
|
|
407
|
+
`system.appearance` and `system.dossier` stay as rendered prose.
|
|
408
|
+
|
|
391
409
|
The Item compiler **dispatches through that same resolved table**, via
|
|
392
410
|
`engine/item-registry.mjs` (`itemTypes()` and `itemBuilder(type)`), so the types a
|
|
393
411
|
consumer's notes are accepted for and the builders they compile with are one
|
|
@@ -534,9 +552,46 @@ Both spellings are equal; the difference is only whether the type brings art.
|
|
|
534
552
|
whitelist without a builder behind it.
|
|
535
553
|
|
|
536
554
|
**The path is spelled the way a note spells it.** Registry art goes through the
|
|
537
|
-
same `resolveImg` rule as a note's `img:`, so
|
|
538
|
-
|
|
539
|
-
|
|
555
|
+
same `resolveImg` rule as a note's `img:`, so one spelling means one thing
|
|
556
|
+
wherever it is written.
|
|
557
|
+
|
|
558
|
+
#### An asset path's first segment says which package owns it
|
|
559
|
+
|
|
560
|
+
Every authored asset path — a registry `img:`, a note's `img:`, an actor's
|
|
561
|
+
`portrait:` — answers "which package holds this file?" in its **first segment**,
|
|
562
|
+
and there are exactly three answers:
|
|
563
|
+
|
|
564
|
+
| Authored path starts with | Owner | Emitted |
|
|
565
|
+
| ------------------------- | --------------------- | -------------------- |
|
|
566
|
+
| `systems/` | a separate **system** | unchanged |
|
|
567
|
+
| `modules/` | a separate **module** | unchanged |
|
|
568
|
+
| anything else | **this package** | `<assetRoot>/<path>` |
|
|
569
|
+
|
|
570
|
+
`assetRoot` is derived, never authored: it is
|
|
571
|
+
`<packageKind>/<foundryPackage>/assets`, and it is the one place `systems/sohl`
|
|
572
|
+
(or `modules/sohl-thalorna`) is ever spelled. So `icons/relic.svg` in a module's
|
|
573
|
+
registry compiles to `modules/sohl-relics/assets/icons/relic.svg`, and the same
|
|
574
|
+
string in the system's compiles to `systems/sohl/assets/icons/relic.svg`. An
|
|
575
|
+
already-served `systems/sohl/assets/icons/…` passes through untouched — which is
|
|
576
|
+
what lets a module pair a SoHL default with one of its own types.
|
|
577
|
+
|
|
578
|
+
**"Anything else" is the rule, not a list of directories.** A package owns its
|
|
579
|
+
whole `assets/` tree, so a directory this toolchain has never heard of is still
|
|
580
|
+
that package's: art under `assets/artwork/` is addressed `artwork/deity.webp`
|
|
581
|
+
and rooted exactly as `icons/…` and `images/…` are. An address naming no package
|
|
582
|
+
at all — an absolute URL, a `data:` URI, a `/`-rooted path — passes through, on
|
|
583
|
+
the same rule rather than as an exception.
|
|
584
|
+
|
|
585
|
+
`worlds/` is deliberately not exempt: a package may not ship art out of a world,
|
|
586
|
+
so prefixing such a path produces a plainly broken one rather than a plausible
|
|
587
|
+
one that 404s in Foundry unreported.
|
|
588
|
+
|
|
589
|
+
**`banner:` is a path that does not follow this rule.** It reaches no compiled
|
|
590
|
+
document; it is a top-level key the Hugo theme reads, and the theme prefixes a
|
|
591
|
+
relative value with `images/` and joins it onto `params.cdnBaseURL`. The two
|
|
592
|
+
address different places — `img:` a file Foundry serves, `banner:` a file the
|
|
593
|
+
CDN serves — so they are stated apart rather than reconciled. See the
|
|
594
|
+
[content format specification](docs/content-format.md#banner-addresses-the-cdn-not-the-foundry-install).
|
|
540
595
|
|
|
541
596
|
#### "Names no art" and "wants no art" are different (#218)
|
|
542
597
|
|
|
@@ -570,12 +625,20 @@ rather than two.
|
|
|
570
625
|
> (`sohl/item-fields.mjs`, "the style of address the office carries"), so one
|
|
571
626
|
> authored key fed two unrelated destinations that disagreed about what empty
|
|
572
627
|
> means — and `title: null` stringified into the compiled document as the literal
|
|
573
|
-
> `"null"`.
|
|
574
|
-
>
|
|
628
|
+
> `"null"`. The field declares `topLevelMeans` now, so the top-level key is no
|
|
629
|
+
> longer a source for it (#218).
|
|
575
630
|
>
|
|
576
|
-
> So `title: null` is
|
|
577
|
-
>
|
|
578
|
-
>
|
|
631
|
+
> So `title: null` is a note declining to state a heading, and the site emitter's
|
|
632
|
+
> `fm.title ?? name` falls back to `name.full`. `title: ""` publishes a
|
|
633
|
+
> deliberately blank heading and is warned about on its own account — as the
|
|
634
|
+
> page's heading, not as an art path.
|
|
635
|
+
>
|
|
636
|
+
> **That warning reads the note's top level only.** The two spellings still name
|
|
637
|
+
> unrelated quantities, so a `sohl.title` — the office's style of address — must
|
|
638
|
+
> not answer for the page's heading. Twenty-eight `sohl-kethira-basic`
|
|
639
|
+
> affiliations write `sohl.title: ""`, meaning an office with no style of
|
|
640
|
+
> address, and every one of them was reported as publishing a blank heading
|
|
641
|
+
> until the check honoured the declaration (#312).
|
|
579
642
|
|
|
580
643
|
Because `""` used to mean "unset", a note still carrying that spelling has
|
|
581
644
|
quietly changed meaning, and the frontmatter lint says so — for either art
|
|
@@ -712,6 +775,14 @@ state the same quantity, which is nearly everywhere: `subType` is the other
|
|
|
712
775
|
declared item field spelled like a note-level key, and there the two agree by
|
|
713
776
|
design.
|
|
714
777
|
|
|
778
|
+
**The statement is symmetric, and the frontmatter linter reads it from the other
|
|
779
|
+
side too.** If the two positions hold unrelated quantities, then the _in-block_
|
|
780
|
+
position is not the note-level field either — so a check about a note-level field
|
|
781
|
+
(the page's heading, an art path) reads past a block key the note's own type
|
|
782
|
+
claims for something else. Declaring `topLevelMeans` settles both directions at
|
|
783
|
+
once; it was read for the emitted field alone until #312, which is how an
|
|
784
|
+
affiliation's office style came to answer for its page heading.
|
|
785
|
+
|
|
715
786
|
**An exempted field is still authorable**, at the two positions that describe the
|
|
716
787
|
document rather than the note:
|
|
717
788
|
|
package/bin/content-build.mjs
CHANGED
|
@@ -136,7 +136,7 @@ import {
|
|
|
136
136
|
locateAddressFinding,
|
|
137
137
|
addressFindingMessage,
|
|
138
138
|
} from "../engine/address-diff.mjs";
|
|
139
|
-
import { itemPackJsonDirs } from "../engine/generate.mjs";
|
|
139
|
+
import { emittedArtFor, itemPackJsonDirs } from "../engine/generate.mjs";
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* The packs `unpack` extracts.
|
|
@@ -799,6 +799,12 @@ function lintCommand() {
|
|
|
799
799
|
// the compile asks the map for whichever pack it is
|
|
800
800
|
// writing, and a companion is a pack it writes.
|
|
801
801
|
packs: config.packDirectories,
|
|
802
|
+
// What art each type actually reaches its document through,
|
|
803
|
+
// asked of the passes rather than listed here (#349). This
|
|
804
|
+
// is the one place that decides what a tree is held to, so
|
|
805
|
+
// it is where the derivation is handed over — the linter
|
|
806
|
+
// states no list of iconless types of its own.
|
|
807
|
+
emittedArt: emittedArtFor,
|
|
802
808
|
references: argv.references,
|
|
803
809
|
});
|
|
804
810
|
|
package/ci/ci-docker.mjs
CHANGED
|
@@ -110,6 +110,27 @@ function exportHead() {
|
|
|
110
110
|
}
|
|
111
111
|
const untar = spawnSync("tar", ["-x", "-C", dir], { input: archive.stdout });
|
|
112
112
|
if (untar.status !== 0) throw new Error("could not unpack the export");
|
|
113
|
+
|
|
114
|
+
// Make the export a repository. `git archive` yields a bare directory, and
|
|
115
|
+
// a workflow step that shells out to git — `git ls-files` in a
|
|
116
|
+
// tracked-artifact check, say — then fails for want of a `.git`, which
|
|
117
|
+
// reads as the check failing rather than as this harness lacking something
|
|
118
|
+
// the runner has. GitHub's own step is a *checkout*, so the faithful export
|
|
119
|
+
// is one too.
|
|
120
|
+
//
|
|
121
|
+
// Initialised and staged rather than committed: `git ls-files` lists the
|
|
122
|
+
// index, so staging every extracted file reproduces exactly the set the
|
|
123
|
+
// runner would see, without needing an identity configured to commit with.
|
|
124
|
+
const init = spawnSync("git", ["init", "-q"], { cwd: dir });
|
|
125
|
+
if (init.status === 0) {
|
|
126
|
+
spawnSync("git", ["add", "-A"], { cwd: dir });
|
|
127
|
+
} else {
|
|
128
|
+
console.error(
|
|
129
|
+
"ci-docker: NOTE — could not make the export a git repository, so a " +
|
|
130
|
+
"workflow step that shells out to git will fail here in a way it " +
|
|
131
|
+
"would not on the runner.",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
113
134
|
return dir;
|
|
114
135
|
}
|
|
115
136
|
|
package/content-config.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* itemBuilders: sohl
|
|
28
28
|
* skipDirectories: [Templates]
|
|
29
29
|
* packs:
|
|
30
|
-
* - { name: items, type: Item
|
|
30
|
+
* - { name: items, type: Item }
|
|
31
31
|
* - { name: journals, type: JournalEntry, label: Journals }
|
|
32
32
|
* packageBuild:
|
|
33
33
|
* assets:
|
|
@@ -69,6 +69,7 @@ import path from "node:path";
|
|
|
69
69
|
// a cycle around a consumer's config file (see `engine/pack-config.mjs`).
|
|
70
70
|
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-charset.mjs";
|
|
71
71
|
import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
|
|
72
|
+
import { ACTOR_TYPES } from "./engine/subtype-registry.mjs";
|
|
72
73
|
import { NOTE_VOCABULARY } from "./engine/note-vocabulary.mjs";
|
|
73
74
|
|
|
74
75
|
/**
|
|
@@ -258,9 +259,6 @@ export function publishesContentPages(config) {
|
|
|
258
259
|
* @property {PackDocumentType} type Foundry document type the pack holds.
|
|
259
260
|
* @property {string} [label] Human-readable label. Defaults to `name`.
|
|
260
261
|
* @property {boolean} [private] Whether the pack is GM-only. Default `false`.
|
|
261
|
-
* @property {string|null} [folders] The pack's folder-hierarchy file, relative
|
|
262
|
-
* to `paths.content`. Default `null` — no
|
|
263
|
-
* folder documents are emitted.
|
|
264
262
|
* @property {string} [prebuilt] Directory holding this pack's per-document
|
|
265
263
|
* JSON, already built. Declaring it skips
|
|
266
264
|
* generation for the pack and compiles from
|
|
@@ -295,7 +293,6 @@ export function publishesContentPages(config) {
|
|
|
295
293
|
* @property {PackDocumentType} type
|
|
296
294
|
* @property {string} label
|
|
297
295
|
* @property {boolean} private
|
|
298
|
-
* @property {string|null} folders
|
|
299
296
|
* @property {string|null} prebuilt
|
|
300
297
|
* @property {string|null} system
|
|
301
298
|
* @property {readonly Readonly<ResolvedPackSpec>[]} companions
|
|
@@ -658,7 +655,6 @@ const PACK_KEYS = [
|
|
|
658
655
|
"type",
|
|
659
656
|
"label",
|
|
660
657
|
"private",
|
|
661
|
-
"folders",
|
|
662
658
|
"companions",
|
|
663
659
|
"mayBeEmpty",
|
|
664
660
|
"default",
|
|
@@ -854,6 +850,19 @@ function optionalString(value, field) {
|
|
|
854
850
|
function normalizePack(value, where, nested = false) {
|
|
855
851
|
if (!isPlainObject(value)) fail(where, "must be an object");
|
|
856
852
|
const pack = /** @type {Record<string, unknown>} */ (value);
|
|
853
|
+
// Retired with the YAML it named (#260). Refused explicitly rather than
|
|
854
|
+
// left to the unknown-key check, because the useful thing to say is not
|
|
855
|
+
// "no such key" but where the folders went: they are notes, and a pack
|
|
856
|
+
// materialises the ones its documents reference.
|
|
857
|
+
if (pack.folders !== undefined) {
|
|
858
|
+
fail(
|
|
859
|
+
`${where}.folders`,
|
|
860
|
+
"is retired — delete it. A folder is a note (`type: folder`) now, " +
|
|
861
|
+
"and a pack materialises the folders its documents reference " +
|
|
862
|
+
"through `packFolder`, so there is no per-pack hierarchy file " +
|
|
863
|
+
"to name",
|
|
864
|
+
);
|
|
865
|
+
}
|
|
857
866
|
rejectUnknownKeys(pack, PACK_KEYS, `${where}.`);
|
|
858
867
|
|
|
859
868
|
const name = requireNonEmptyString(pack.name, `${where}.name`);
|
|
@@ -865,10 +874,6 @@ function normalizePack(value, where, nested = false) {
|
|
|
865
874
|
fail(`${where}.type`, `must be one of: ${PACK_DOCUMENT_TYPES.join(", ")}`);
|
|
866
875
|
}
|
|
867
876
|
|
|
868
|
-
if (pack.folders !== undefined && pack.folders !== null) {
|
|
869
|
-
requireNonEmptyString(pack.folders, `${where}.folders`);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
877
|
const companionsInput = pack.companions;
|
|
873
878
|
if (companionsInput !== undefined && !Array.isArray(companionsInput)) {
|
|
874
879
|
fail(`${where}.companions`, "must be an array");
|
|
@@ -893,8 +898,8 @@ function normalizePack(value, where, nested = false) {
|
|
|
893
898
|
|
|
894
899
|
// A prebuilt pack's per-document JSON already exists, so it has no
|
|
895
900
|
// generation pass. Every key below describes one, which is why none of them
|
|
896
|
-
// may accompany it: silently ignoring a
|
|
897
|
-
//
|
|
901
|
+
// may accompany it: silently ignoring a key that can never be read is
|
|
902
|
+
// worse than refusing the configuration that declares it.
|
|
898
903
|
const prebuilt =
|
|
899
904
|
pack.prebuilt === undefined || pack.prebuilt === null ?
|
|
900
905
|
null
|
|
@@ -907,13 +912,6 @@ function normalizePack(value, where, nested = false) {
|
|
|
907
912
|
"another pack's pass, and a prebuilt pack has no pass",
|
|
908
913
|
);
|
|
909
914
|
}
|
|
910
|
-
if (pack.folders !== undefined && pack.folders !== null) {
|
|
911
|
-
fail(
|
|
912
|
-
`${where}.folders`,
|
|
913
|
-
"may not accompany `prebuilt`: the folder hierarchy is built " +
|
|
914
|
-
"during generation, which a prebuilt pack skips",
|
|
915
|
-
);
|
|
916
|
-
}
|
|
917
915
|
if (Array.isArray(companionsInput) && companionsInput.length) {
|
|
918
916
|
fail(
|
|
919
917
|
`${where}.companions`,
|
|
@@ -946,10 +944,6 @@ function normalizePack(value, where, nested = false) {
|
|
|
946
944
|
label:
|
|
947
945
|
pack.label === undefined ? name : requireNonEmptyString(pack.label, `${where}.label`),
|
|
948
946
|
private: optionalBoolean(pack.private, `${where}.private`, false),
|
|
949
|
-
folders:
|
|
950
|
-
pack.folders === undefined || pack.folders === null ?
|
|
951
|
-
null
|
|
952
|
-
: /** @type {string} */ (pack.folders),
|
|
953
947
|
companions: Object.freeze(companions),
|
|
954
948
|
mayBeEmpty: optionalBoolean(pack.mayBeEmpty, `${where}.mayBeEmpty`, false),
|
|
955
949
|
// Which pack of a type receives a note that declares none. Validated
|
|
@@ -1960,7 +1954,15 @@ export function defineConfig(config) {
|
|
|
1960
1954
|
// holds every key any of them declares, so this stays "the registry's keys"
|
|
1961
1955
|
// rather than becoming a second list to keep in step (#1504).
|
|
1962
1956
|
const itemTypes = Object.freeze(new Set(Object.keys(itemBuilders)));
|
|
1963
|
-
|
|
1957
|
+
// Every note that compiles into a *system-bearing* document publishes its
|
|
1958
|
+
// prose as a documentation JournalEntry, and that includes actors (#337).
|
|
1959
|
+
// A being was the one such note with no `none` address — its only address
|
|
1960
|
+
// named the Actor — so nothing a prose link wrote could land on its page.
|
|
1961
|
+
// `doc` stays out for the reason that actually applies to it: its single
|
|
1962
|
+
// document *is* the prose.
|
|
1963
|
+
const docEntryTypes = Object.freeze(
|
|
1964
|
+
new Set([...itemTypes, ...ACTOR_TYPES, "macro", ...MAP_TYPES]),
|
|
1965
|
+
);
|
|
1964
1966
|
|
|
1965
1967
|
return Object.freeze({
|
|
1966
1968
|
rootDir,
|