@heroiclands/package-build 18.1.1 → 19.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 +153 -0
- package/CONTENT.md +2 -2
- package/ci/ci-docker.mjs +236 -0
- package/ci/ci-steps.mjs +249 -0
- package/content-config.mjs +16 -23
- package/docs/content-format.md +14 -13
- package/engine/base-compiler.mjs +2 -0
- package/engine/folder-notes.mjs +64 -0
- package/engine/foundry-entries.mjs +2 -0
- package/engine/frontmatter-lint.mjs +29 -1
- package/engine/frontmatter.mjs +1 -1
- package/engine/generate.mjs +11 -36
- package/engine/helpers.mjs +1 -119
- package/engine/item-compiler.mjs +7 -9
- package/engine/journals.mjs +6 -12
- package/engine/scenes.mjs +7 -9
- package/githooks/commit-msg +40 -0
- package/githooks/hook-enabled.sh +25 -0
- package/githooks/pre-commit +11 -0
- package/githooks/pre-merge-commit +12 -0
- package/githooks/pre-push +109 -0
- package/githooks/protected-branch.sh +62 -0
- package/hm3/actors.mjs +7 -10
- package/package.json +3 -1
- package/types/content-config.d.mts +0 -7
- package/types/engine/folder-notes.d.mts +39 -0
- package/types/engine/helpers.d.mts +0 -29
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:
|
|
@@ -258,9 +258,6 @@ export function publishesContentPages(config) {
|
|
|
258
258
|
* @property {PackDocumentType} type Foundry document type the pack holds.
|
|
259
259
|
* @property {string} [label] Human-readable label. Defaults to `name`.
|
|
260
260
|
* @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
261
|
* @property {string} [prebuilt] Directory holding this pack's per-document
|
|
265
262
|
* JSON, already built. Declaring it skips
|
|
266
263
|
* generation for the pack and compiles from
|
|
@@ -295,7 +292,6 @@ export function publishesContentPages(config) {
|
|
|
295
292
|
* @property {PackDocumentType} type
|
|
296
293
|
* @property {string} label
|
|
297
294
|
* @property {boolean} private
|
|
298
|
-
* @property {string|null} folders
|
|
299
295
|
* @property {string|null} prebuilt
|
|
300
296
|
* @property {string|null} system
|
|
301
297
|
* @property {readonly Readonly<ResolvedPackSpec>[]} companions
|
|
@@ -658,7 +654,6 @@ const PACK_KEYS = [
|
|
|
658
654
|
"type",
|
|
659
655
|
"label",
|
|
660
656
|
"private",
|
|
661
|
-
"folders",
|
|
662
657
|
"companions",
|
|
663
658
|
"mayBeEmpty",
|
|
664
659
|
"default",
|
|
@@ -854,6 +849,19 @@ function optionalString(value, field) {
|
|
|
854
849
|
function normalizePack(value, where, nested = false) {
|
|
855
850
|
if (!isPlainObject(value)) fail(where, "must be an object");
|
|
856
851
|
const pack = /** @type {Record<string, unknown>} */ (value);
|
|
852
|
+
// Retired with the YAML it named (#260). Refused explicitly rather than
|
|
853
|
+
// left to the unknown-key check, because the useful thing to say is not
|
|
854
|
+
// "no such key" but where the folders went: they are notes, and a pack
|
|
855
|
+
// materialises the ones its documents reference.
|
|
856
|
+
if (pack.folders !== undefined) {
|
|
857
|
+
fail(
|
|
858
|
+
`${where}.folders`,
|
|
859
|
+
"is retired — delete it. A folder is a note (`type: folder`) now, " +
|
|
860
|
+
"and a pack materialises the folders its documents reference " +
|
|
861
|
+
"through `packFolder`, so there is no per-pack hierarchy file " +
|
|
862
|
+
"to name",
|
|
863
|
+
);
|
|
864
|
+
}
|
|
857
865
|
rejectUnknownKeys(pack, PACK_KEYS, `${where}.`);
|
|
858
866
|
|
|
859
867
|
const name = requireNonEmptyString(pack.name, `${where}.name`);
|
|
@@ -865,10 +873,6 @@ function normalizePack(value, where, nested = false) {
|
|
|
865
873
|
fail(`${where}.type`, `must be one of: ${PACK_DOCUMENT_TYPES.join(", ")}`);
|
|
866
874
|
}
|
|
867
875
|
|
|
868
|
-
if (pack.folders !== undefined && pack.folders !== null) {
|
|
869
|
-
requireNonEmptyString(pack.folders, `${where}.folders`);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
876
|
const companionsInput = pack.companions;
|
|
873
877
|
if (companionsInput !== undefined && !Array.isArray(companionsInput)) {
|
|
874
878
|
fail(`${where}.companions`, "must be an array");
|
|
@@ -893,8 +897,8 @@ function normalizePack(value, where, nested = false) {
|
|
|
893
897
|
|
|
894
898
|
// A prebuilt pack's per-document JSON already exists, so it has no
|
|
895
899
|
// generation pass. Every key below describes one, which is why none of them
|
|
896
|
-
// may accompany it: silently ignoring a
|
|
897
|
-
//
|
|
900
|
+
// may accompany it: silently ignoring a key that can never be read is
|
|
901
|
+
// worse than refusing the configuration that declares it.
|
|
898
902
|
const prebuilt =
|
|
899
903
|
pack.prebuilt === undefined || pack.prebuilt === null ?
|
|
900
904
|
null
|
|
@@ -907,13 +911,6 @@ function normalizePack(value, where, nested = false) {
|
|
|
907
911
|
"another pack's pass, and a prebuilt pack has no pass",
|
|
908
912
|
);
|
|
909
913
|
}
|
|
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
914
|
if (Array.isArray(companionsInput) && companionsInput.length) {
|
|
918
915
|
fail(
|
|
919
916
|
`${where}.companions`,
|
|
@@ -946,10 +943,6 @@ function normalizePack(value, where, nested = false) {
|
|
|
946
943
|
label:
|
|
947
944
|
pack.label === undefined ? name : requireNonEmptyString(pack.label, `${where}.label`),
|
|
948
945
|
private: optionalBoolean(pack.private, `${where}.private`, false),
|
|
949
|
-
folders:
|
|
950
|
-
pack.folders === undefined || pack.folders === null ?
|
|
951
|
-
null
|
|
952
|
-
: /** @type {string} */ (pack.folders),
|
|
953
946
|
companions: Object.freeze(companions),
|
|
954
947
|
mayBeEmpty: optionalBoolean(pack.mayBeEmpty, `${where}.mayBeEmpty`, false),
|
|
955
948
|
// Which pack of a type receives a note that declares none. Validated
|
package/docs/content-format.md
CHANGED
|
@@ -507,12 +507,10 @@ of the same entry is a warning; the declaration still works.
|
|
|
507
507
|
|
|
508
508
|
#### The compendium folder
|
|
509
509
|
|
|
510
|
-
A note says which folder of its pack it lands in
|
|
511
|
-
`packFolder` wins where both are present:
|
|
510
|
+
A note says which folder of its pack it lands in:
|
|
512
511
|
|
|
513
512
|
```yaml
|
|
514
513
|
packFolder: poisonsandtoxins # a folder note's address
|
|
515
|
-
folder: ONXsqZAIZr2qzxTb # a Foundry id
|
|
516
514
|
```
|
|
517
515
|
|
|
518
516
|
**`packFolder` is a folder note's address** — an ordinary address, resolved the
|
|
@@ -522,13 +520,6 @@ complete address here; `folder-poisonsandtoxins` and the fully qualified
|
|
|
522
520
|
`sohl-none-folder-poisonsandtoxins` name the same folder. An address no folder
|
|
523
521
|
note answers to is a build error naming the folders the package does declare.
|
|
524
522
|
|
|
525
|
-
**`folder` is a Foundry id**, and is unchanged: a note that names one is read,
|
|
526
|
-
resolved and emitted exactly as before.
|
|
527
|
-
|
|
528
|
-
**Which one a value is comes from the field it was written in, never from the
|
|
529
|
-
string.** Both are alphanumeric, so there is nothing in the value to tell them
|
|
530
|
-
apart.
|
|
531
|
-
|
|
532
523
|
Note this is the _pack_ folder, not the note's directory. The directory is
|
|
533
524
|
`file.path` / `file.folder`, which a content table reads separately.
|
|
534
525
|
|
|
@@ -552,6 +543,14 @@ the first.
|
|
|
552
543
|
> path form is **removed**, not deprecated: nothing authored it yet, which is
|
|
553
544
|
> the whole reason the change was cheap enough to make.
|
|
554
545
|
|
|
546
|
+
> **`folder:` was a Foundry id**, resolved against a per-pack
|
|
547
|
+
> `*-folders.yaml` — five files per tree. Both halves are **retired** together
|
|
548
|
+
> (#260): the id spelling has nothing left to resolve against once the YAML is
|
|
549
|
+
> gone, and the YAML has no reader once the spelling is refused. A note that
|
|
550
|
+
> still writes `folder:` fails the build, naming `packFolder` and the line to
|
|
551
|
+
> rewrite, rather than being ignored — a retired field left ignored reads to
|
|
552
|
+
> its author as though it still works.
|
|
553
|
+
|
|
555
554
|
#### The knowledgebase category
|
|
556
555
|
|
|
557
556
|
`kbcat` names the group a note is listed under on the knowledgebase and the
|
|
@@ -962,8 +961,9 @@ relational operations:
|
|
|
962
961
|
`_section` is why one query replaces the forty near-identical blocks a grouped
|
|
963
962
|
table used to need: the authored `ORDER BY` decides the section order too.
|
|
964
963
|
|
|
965
|
-
**Beware `
|
|
966
|
-
directory is `file.folder`.
|
|
964
|
+
**Beware `packFolder`.** It is a note's _pack_ folder, not its directory — the
|
|
965
|
+
directory is `file.folder`. (The `folder` field it replaced is retired; a query
|
|
966
|
+
naming it matches nothing.)
|
|
967
967
|
|
|
968
968
|
###### Reading another package's notes
|
|
969
969
|
|
|
@@ -2125,7 +2125,8 @@ an Adventure pack declared first still compiles last.
|
|
|
2125
2125
|
|
|
2126
2126
|
Foundry's `Folder` — the grouping documents are filed in, and the last document
|
|
2127
2127
|
this package compiled from bespoke configuration (`*-folders.yaml`, five files
|
|
2128
|
-
per tree) rather than from a note.
|
|
2128
|
+
per tree) rather than from a note. Those files are retired (#260); a pack that
|
|
2129
|
+
still names one is refused.
|
|
2129
2130
|
|
|
2130
2131
|
```yaml
|
|
2131
2132
|
---
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
79
79
|
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
80
80
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
81
|
+
import { assertNoDeclaredFolder } from "./folder-notes.mjs";
|
|
81
82
|
import {
|
|
82
83
|
assertNoAliasesField,
|
|
83
84
|
assertNoDraftField,
|
|
@@ -895,6 +896,7 @@ export class BasePackCompiler {
|
|
|
895
896
|
// neither message may repeat it.
|
|
896
897
|
try {
|
|
897
898
|
assertNoDeclaredPackage(fm, { absPath });
|
|
899
|
+
assertNoDeclaredFolder(fm, { absPath });
|
|
898
900
|
assertNoDraftField(fm, { absPath });
|
|
899
901
|
assertNoAliasesField(fm, { absPath });
|
|
900
902
|
assertNoSectionField(fm, { absPath });
|
package/engine/folder-notes.mjs
CHANGED
|
@@ -56,6 +56,7 @@ import log from "loglevel";
|
|
|
56
56
|
import { NO_SYSTEM, canonicalKey } from "./content-address.mjs";
|
|
57
57
|
import { isAddressSegment } from "./address-charset.mjs";
|
|
58
58
|
import { makeId } from "./ids.mjs";
|
|
59
|
+
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* The note type a folder is authored as.
|
|
@@ -468,3 +469,66 @@ export function folderDocument(folder, parent, documentType, stats) {
|
|
|
468
469
|
_key: `!folders!${folder.id}`,
|
|
469
470
|
};
|
|
470
471
|
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Refuse a note that declares the retired `folder:` spelling.
|
|
475
|
+
*
|
|
476
|
+
* `folder:` named a compendium folder by the raw Foundry id declared in a
|
|
477
|
+
* per-pack `*-folders.yaml`. Both halves are retired together (#260): the id
|
|
478
|
+
* spelling has nothing left to resolve against once the YAML is gone, and the
|
|
479
|
+
* YAML has no reader once the spelling is refused.
|
|
480
|
+
*
|
|
481
|
+
* **Presence is the whole test.** An empty `folder:` — which parses as `null`
|
|
482
|
+
* — is still the field, and a value that happens to match a folder note's id
|
|
483
|
+
* is still the retired spelling. There is no value that makes writing it
|
|
484
|
+
* correct, so the message says what to write instead rather than which value
|
|
485
|
+
* to change.
|
|
486
|
+
*
|
|
487
|
+
* **Both positions**, because notes wrote it both ways: top-level, and inside
|
|
488
|
+
* the `sohl:` block. Checking only the more common one is how a sweep leaves
|
|
489
|
+
* a tail behind.
|
|
490
|
+
*
|
|
491
|
+
* Refused rather than ignored, on the pattern `package:` set: a retired field
|
|
492
|
+
* left ignored reads to its author as though it still works — the note says
|
|
493
|
+
* one thing and the build does another, and nothing says so.
|
|
494
|
+
*
|
|
495
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
496
|
+
* could not be parsed.
|
|
497
|
+
* @param {object} [options] - Options.
|
|
498
|
+
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
499
|
+
* it where the caller emits through a diagnostic, which puts the locator at
|
|
500
|
+
* the start of the line already — repeating it prints the path twice.
|
|
501
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on
|
|
502
|
+
* the failing path to locate the offending line and column. The position
|
|
503
|
+
* rides on the thrown error as `position`, for a caller that emits a
|
|
504
|
+
* diagnostic.
|
|
505
|
+
* @returns {void}
|
|
506
|
+
* @throws {Error} When the note declares the field.
|
|
507
|
+
*/
|
|
508
|
+
export function assertNoDeclaredFolder(fm, { file, absPath } = {}) {
|
|
509
|
+
if (!fm || typeof fm !== "object") return;
|
|
510
|
+
const sohl = fm.sohl;
|
|
511
|
+
const inBlock = !!sohl && typeof sohl === "object" && Object.hasOwn(sohl, "folder");
|
|
512
|
+
if (!Object.hasOwn(fm, "folder") && !inBlock) return;
|
|
513
|
+
|
|
514
|
+
const declared = inBlock ? sohl.folder : fm.folder;
|
|
515
|
+
const wrote =
|
|
516
|
+
declared === null || declared === undefined || declared === "" ?
|
|
517
|
+
"`folder:`"
|
|
518
|
+
: `\`folder: ${declared}\``;
|
|
519
|
+
|
|
520
|
+
const err = new Error(
|
|
521
|
+
`${wrote} is a retired frontmatter field — write \`packFolder\` ` +
|
|
522
|
+
`instead` +
|
|
523
|
+
(file ? ` — ${file}` : "") +
|
|
524
|
+
`. A folder is a note now, and \`packFolder\` names it by its ` +
|
|
525
|
+
`address (\`packFolder: miscgear\`), not by the Foundry id a ` +
|
|
526
|
+
`retired \`*-folders.yaml\` used to declare.`,
|
|
527
|
+
);
|
|
528
|
+
// Where the field is, so the caller's diagnostic opens on the line that
|
|
529
|
+
// has to be rewritten. Read here rather than carried through every walk:
|
|
530
|
+
// this is the failing path, and the build stops on it.
|
|
531
|
+
const position = locateFrontmatterKey(absPath, "folder");
|
|
532
|
+
if (position) err.position = position;
|
|
533
|
+
throw err;
|
|
534
|
+
}
|
|
@@ -71,6 +71,7 @@ import { compendiumUuid, currentType, packForType, pageUuid } from "./ids.mjs";
|
|
|
71
71
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
72
72
|
import { isHomepage } from "./homepage.mjs";
|
|
73
73
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
74
|
+
import { assertNoDeclaredFolder } from "./folder-notes.mjs";
|
|
74
75
|
import {
|
|
75
76
|
assertNoAliasesField,
|
|
76
77
|
assertNoDraftField,
|
|
@@ -273,6 +274,7 @@ export function collectFoundryEntries(contentBase, ctx) {
|
|
|
273
274
|
absPath,
|
|
274
275
|
configured: ctx.contentPackage,
|
|
275
276
|
});
|
|
277
|
+
assertNoDeclaredFolder(fm, { file: rel, absPath });
|
|
276
278
|
assertNoDraftField(fm, { file: rel, absPath });
|
|
277
279
|
assertNoAliasesField(fm, { file: rel, absPath });
|
|
278
280
|
assertNoSectionField(fm, { file: rel, absPath });
|
|
@@ -114,7 +114,7 @@ import {
|
|
|
114
114
|
* @type {ReadonlySet<string>}
|
|
115
115
|
*/
|
|
116
116
|
export const UNIVERSAL_KEYS = Object.freeze(
|
|
117
|
-
new Set(["
|
|
117
|
+
new Set(["packFolder", "pack", "archetype", "templatePriority", "kbcat"]),
|
|
118
118
|
);
|
|
119
119
|
|
|
120
120
|
/**
|
|
@@ -744,6 +744,34 @@ export function lintNote(
|
|
|
744
744
|
"note in the tree belongs to it",
|
|
745
745
|
});
|
|
746
746
|
}
|
|
747
|
+
// `folder:` named a compendium folder by the raw Foundry id declared in a
|
|
748
|
+
// per-pack `*-folders.yaml`. Both halves are retired together (#260): the
|
|
749
|
+
// id spelling has nothing left to resolve against once the YAML is gone.
|
|
750
|
+
//
|
|
751
|
+
// Checked here as well as refused at compile because this is where an
|
|
752
|
+
// author meets every one of them in the tree at once — which is what a
|
|
753
|
+
// tree still to sweep needs, the whole corpus rather than the first note
|
|
754
|
+
// the compile happens to reach.
|
|
755
|
+
//
|
|
756
|
+
// **Both positions**, because notes wrote it both ways: top-level, and
|
|
757
|
+
// inside the `sohl:` block. The block spelling is no longer a universal
|
|
758
|
+
// key, so it would otherwise be reported as merely unrecognized, which
|
|
759
|
+
// says nothing about what to write instead.
|
|
760
|
+
const sohlBlock = fm.sohl;
|
|
761
|
+
const folderInBlock =
|
|
762
|
+
!!sohlBlock && typeof sohlBlock === "object" && Object.hasOwn(sohlBlock, "folder");
|
|
763
|
+
if (Object.hasOwn(fm, "folder") || folderInBlock) {
|
|
764
|
+
findings.push({
|
|
765
|
+
file: note.file,
|
|
766
|
+
...at("folder"),
|
|
767
|
+
severity: "error",
|
|
768
|
+
message:
|
|
769
|
+
"`folder:` is a retired frontmatter field — write `packFolder` " +
|
|
770
|
+
"instead. A folder is a note (`type: folder`) now, and " +
|
|
771
|
+
"`packFolder` names it by its address, not by the Foundry id a " +
|
|
772
|
+
"retired `*-folders.yaml` used to declare",
|
|
773
|
+
});
|
|
774
|
+
}
|
|
747
775
|
// `img: ""` was how a note said "I name no art" while `resolveImg`
|
|
748
776
|
// conflated the two empties and every caller defaulted with `||`. It now
|
|
749
777
|
// says the opposite — "ship no art, and do not default me" (#218) — so a
|
package/engine/frontmatter.mjs
CHANGED
|
@@ -329,5 +329,5 @@ export function parseValueDesc(raw) {
|
|
|
329
329
|
export function folderField(fm) {
|
|
330
330
|
const asAddress = sohlField(fm, "packFolder", null);
|
|
331
331
|
if (asAddress != null && asAddress !== "") return { value: asAddress, isAddress: true };
|
|
332
|
-
return { value:
|
|
332
|
+
return { value: null, isAddress: true };
|
|
333
333
|
}
|
package/engine/generate.mjs
CHANGED
|
@@ -51,14 +51,7 @@ import { Hm3Actors } from "../hm3/actors.mjs";
|
|
|
51
51
|
import { Macros } from "./macros.mjs";
|
|
52
52
|
import { Scenes } from "./scenes.mjs";
|
|
53
53
|
import { Bundles } from "./bundles.mjs";
|
|
54
|
-
import {
|
|
55
|
-
statsForPack,
|
|
56
|
-
loadFolders,
|
|
57
|
-
buildFolderResolver,
|
|
58
|
-
writeFolderDocs,
|
|
59
|
-
parseMarkdownFile,
|
|
60
|
-
folderFilename,
|
|
61
|
-
} from "./helpers.mjs";
|
|
54
|
+
import { statsForPack, parseMarkdownFile, folderFilename } from "./helpers.mjs";
|
|
62
55
|
import {
|
|
63
56
|
buildFolderNoteIndex,
|
|
64
57
|
collectFolderNotes,
|
|
@@ -359,7 +352,7 @@ export function unsatisfiedPassDependencies(running, config) {
|
|
|
359
352
|
* count (0 on success) and the number of entries it wrote.
|
|
360
353
|
*/
|
|
361
354
|
async function generatePack(
|
|
362
|
-
{ name, type,
|
|
355
|
+
{ name, type, companions, system },
|
|
363
356
|
config,
|
|
364
357
|
router,
|
|
365
358
|
routingReporter,
|
|
@@ -380,41 +373,27 @@ async function generatePack(
|
|
|
380
373
|
|
|
381
374
|
log.info(`Pack ${name}: ${contentBase} → ${dest}`);
|
|
382
375
|
|
|
383
|
-
let folderList;
|
|
384
|
-
let yamlResolver;
|
|
385
|
-
try {
|
|
386
|
-
folderList = folders ? loadFolders(path.join(contentBase, folders)) : [];
|
|
387
|
-
({ resolver: yamlResolver } = buildFolderResolver(folderList));
|
|
388
|
-
} catch (err) {
|
|
389
|
-
log.error(`${name} ${folders} validation failed: ${err.message}`);
|
|
390
|
-
return { errors: 1, compiled: 0 };
|
|
391
|
-
}
|
|
392
|
-
|
|
393
376
|
// Which folder notes this pack turned out to hold something for. A folder
|
|
394
377
|
// materialises in every pack holding a document that references it, so the
|
|
395
|
-
// set is not knowable until the pass has compiled
|
|
396
|
-
// documents are written after `compile()`
|
|
397
|
-
// (#257).
|
|
378
|
+
// set is not knowable until the pass has compiled, which is why these
|
|
379
|
+
// documents are written after `compile()` (#257).
|
|
398
380
|
/** @type {Set<import("./folder-notes.mjs").FolderNote>} */
|
|
399
381
|
const referencedFolders = new Set();
|
|
400
382
|
|
|
401
383
|
/**
|
|
402
|
-
* The Foundry folder id a note names, by
|
|
384
|
+
* The Foundry folder id a note names, by its folder note's address.
|
|
403
385
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
386
|
+
* There is one spelling. `packFolder` names a folder **note**, resolved
|
|
387
|
+
* through the address index shared by the whole build; the `folder:`
|
|
388
|
+
* Foundry-id spelling and the per-pack `*-folders.yaml` it resolved
|
|
389
|
+
* against are retired together (#260), so there is no second source left
|
|
390
|
+
* for a value to come from.
|
|
409
391
|
*
|
|
410
392
|
* @param {string|null|undefined} value - As authored.
|
|
411
|
-
* @param {object} [opts]
|
|
412
|
-
* @param {boolean} [opts.isAddress] - Whether `value` is a folder address.
|
|
413
393
|
* @returns {string|null} The folder id, or `null` for an absent value.
|
|
414
394
|
*/
|
|
415
|
-
const resolver = (value
|
|
395
|
+
const resolver = (value) => {
|
|
416
396
|
if (value == null || value === "") return null;
|
|
417
|
-
if (!isAddress) return yamlResolver(value);
|
|
418
397
|
const folder = folderNotes.resolve(value);
|
|
419
398
|
// Its ancestors with it: a `Folder` whose parent is absent from the
|
|
420
399
|
// pack is an orphan Foundry renders at the root, so materialising a
|
|
@@ -444,10 +423,6 @@ async function generatePack(
|
|
|
444
423
|
companionDests[companion.name] = companionDest;
|
|
445
424
|
}
|
|
446
425
|
|
|
447
|
-
// A folder document belongs to the pack it is written into, so it carries
|
|
448
|
-
// that pack's system rather than the package-wide one (#48).
|
|
449
|
-
writeFolderDocs(folderList, statsForPack(system, config), dest, type);
|
|
450
|
-
|
|
451
426
|
const pack = new packClass({
|
|
452
427
|
contentBase,
|
|
453
428
|
dest,
|
package/engine/helpers.mjs
CHANGED
|
@@ -913,102 +913,9 @@ export function expandNoteTables(body, { docs, name, fm, bodyLine, sqlTables })
|
|
|
913
913
|
}
|
|
914
914
|
|
|
915
915
|
/* ------------------------------------------------------------------------ */
|
|
916
|
-
/* Folder
|
|
916
|
+
/* Folder document filenames */
|
|
917
917
|
/* ------------------------------------------------------------------------ */
|
|
918
918
|
|
|
919
|
-
/**
|
|
920
|
-
* Loads a folders.yaml file as an array of folder entries. Returns []
|
|
921
|
-
* when the file is missing (logging a warning) so packs without folders
|
|
922
|
-
* can opt out simply by not committing the file.
|
|
923
|
-
*/
|
|
924
|
-
export function loadFolders(foldersFile) {
|
|
925
|
-
if (!fs.existsSync(foldersFile)) {
|
|
926
|
-
log.warn(`No folders.yaml at ${foldersFile}; no folders will be emitted`);
|
|
927
|
-
return [];
|
|
928
|
-
}
|
|
929
|
-
const raw = fs.readFileSync(foldersFile, "utf8");
|
|
930
|
-
const parsed = yaml.parse(raw);
|
|
931
|
-
if (parsed == null) return [];
|
|
932
|
-
if (!Array.isArray(parsed)) {
|
|
933
|
-
throw new Error(`folders.yaml must contain a YAML list; got ${typeof parsed}`);
|
|
934
|
-
}
|
|
935
|
-
return parsed;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* Validates folder invariants and returns a resolver function that maps a
|
|
940
|
-
* folder id to the same id (after verifying it exists). Returns `null` for
|
|
941
|
-
* a null/empty input; throws for an unknown id.
|
|
942
|
-
*
|
|
943
|
-
* Invariants:
|
|
944
|
-
* - Every folder must have a non-empty id
|
|
945
|
-
* - Every folder must have a name
|
|
946
|
-
* - Sibling folders (same parentFolderId) must have unique names
|
|
947
|
-
* - Every parentFolderId must match an existing folder id (or be "")
|
|
948
|
-
*
|
|
949
|
-
* Returns { resolver, folders } where folders is the validated list.
|
|
950
|
-
*/
|
|
951
|
-
export function buildFolderResolver(folders) {
|
|
952
|
-
const byId = new Map();
|
|
953
|
-
for (const f of folders) {
|
|
954
|
-
if (!f.id) {
|
|
955
|
-
throw new Error(`Folder missing id: ${JSON.stringify(f)}`);
|
|
956
|
-
}
|
|
957
|
-
if (!f.name) {
|
|
958
|
-
throw new Error(`Folder ${f.id} missing name`);
|
|
959
|
-
}
|
|
960
|
-
if (byId.has(f.id)) {
|
|
961
|
-
throw new Error(`Duplicate folder id ${f.id}`);
|
|
962
|
-
}
|
|
963
|
-
byId.set(f.id, f);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const siblingsByParent = new Map();
|
|
967
|
-
for (const f of folders) {
|
|
968
|
-
const parentId = f.parentFolderId || "";
|
|
969
|
-
if (parentId && !byId.has(parentId)) {
|
|
970
|
-
throw new Error(
|
|
971
|
-
`Folder ${f.id} (${f.name}) references unknown parentFolderId ${parentId}`,
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
if (!siblingsByParent.has(parentId)) {
|
|
975
|
-
siblingsByParent.set(parentId, new Set());
|
|
976
|
-
}
|
|
977
|
-
const siblings = siblingsByParent.get(parentId);
|
|
978
|
-
if (siblings.has(f.name)) {
|
|
979
|
-
throw new Error(
|
|
980
|
-
`Sibling folders share name "${f.name}" under parent ${parentId || "(root)"} — names must be unique among siblings`,
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
siblings.add(f.name);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* The folder id a note names, by id.
|
|
988
|
-
*
|
|
989
|
-
* **Only by id.** This resolver answers for `folder:` alone; `packFolder:`
|
|
990
|
-
* names a folder *note* and is resolved through the address index instead
|
|
991
|
-
* (#255). The path lookup that briefly lived here is gone with the path
|
|
992
|
-
* spelling it served — it was never released, so there is nothing to
|
|
993
|
-
* deprecate.
|
|
994
|
-
*
|
|
995
|
-
* @param {string|null|undefined} value - As authored.
|
|
996
|
-
* @returns {string|null} The id, or `null` for an absent value.
|
|
997
|
-
* @throws {Error} When the id is not one this pack declares.
|
|
998
|
-
*/
|
|
999
|
-
function resolver(value) {
|
|
1000
|
-
if (value == null || value === "") return null;
|
|
1001
|
-
const authored = String(value).trim();
|
|
1002
|
-
if (!authored) return null;
|
|
1003
|
-
if (!byId.has(authored)) {
|
|
1004
|
-
throw new Error(`Unknown folder id "${authored}"`);
|
|
1005
|
-
}
|
|
1006
|
-
return authored;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
return { resolver, folders };
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
919
|
/**
|
|
1013
920
|
* Builds a compendium-source filename for a folder JSON document:
|
|
1014
921
|
* `folder_Name_id.json` with non-alphanumeric runs replaced by
|
|
@@ -1017,28 +924,3 @@ export function buildFolderResolver(folders) {
|
|
|
1017
924
|
export function folderFilename(name, id) {
|
|
1018
925
|
return `folder_${unidecode(name)}_${id}`.replace(/[^0-9a-zA-Z]+/g, "_") + ".json";
|
|
1019
926
|
}
|
|
1020
|
-
|
|
1021
|
-
/**
|
|
1022
|
-
* Writes one JSON document per folder into `destDir`. `documentType`
|
|
1023
|
-
* determines the folder's Foundry `type` field — `"Item"` for the items
|
|
1024
|
-
* pack, `"JournalEntry"` for the journals pack.
|
|
1025
|
-
*/
|
|
1026
|
-
export function writeFolderDocs(folders, stats, destDir, documentType) {
|
|
1027
|
-
for (const folder of folders) {
|
|
1028
|
-
const doc = {
|
|
1029
|
-
name: folder.name,
|
|
1030
|
-
sorting: "a",
|
|
1031
|
-
folder: folder.parentFolderId || null,
|
|
1032
|
-
type: documentType,
|
|
1033
|
-
_id: folder.id,
|
|
1034
|
-
sort: 0,
|
|
1035
|
-
color: folder.color,
|
|
1036
|
-
flags: folder.flags || {},
|
|
1037
|
-
_stats: stats,
|
|
1038
|
-
_key: `!folders!${folder.id}`,
|
|
1039
|
-
};
|
|
1040
|
-
const outPath = path.join(destDir, folderFilename(folder.name, folder.id));
|
|
1041
|
-
fs.writeFileSync(outPath, JSON.stringify(doc, null, 2), "utf8");
|
|
1042
|
-
}
|
|
1043
|
-
log.info(`Emitted ${folders.length} folder document(s) to ${destDir}`);
|
|
1044
|
-
}
|
package/engine/item-compiler.mjs
CHANGED
|
@@ -296,15 +296,13 @@ export class SystemItemCompiler extends BasePackCompiler {
|
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
const effects = blockProperty(fm, system, "effects");
|
|
299
|
-
// Read through the system block like every other item field
|
|
300
|
-
//
|
|
301
|
-
// folder
|
|
302
|
-
//
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.folderResolver(packFolderAddress, { isAddress: true })
|
|
307
|
-
: this.folderResolver(blockField(fm, system, "folder", null));
|
|
299
|
+
// Read through the system block like every other item field. There is
|
|
300
|
+
// one spelling: `packFolder` names a folder note by its address, the
|
|
301
|
+
// `folder:` id spelling having been retired with the per-pack YAML it
|
|
302
|
+
// resolved against (#251, #255, #260).
|
|
303
|
+
const folder = this.folderResolver(blockField(fm, system, "packFolder", null), {
|
|
304
|
+
isAddress: true,
|
|
305
|
+
});
|
|
308
306
|
|
|
309
307
|
return {
|
|
310
308
|
name,
|
package/engine/journals.mjs
CHANGED
|
@@ -381,12 +381,8 @@ export class Journals extends BasePackCompiler {
|
|
|
381
381
|
|
|
382
382
|
// A documentation entry is filed exactly where the document it
|
|
383
383
|
// describes is, so the journals pack mirrors the items pack and a doc
|
|
384
|
-
// sits under the same heading a reader found the item under.
|
|
385
|
-
//
|
|
386
|
-
// against this pack's own folders.yaml — an item folder is declared in
|
|
387
|
-
// the items one, a macro folder in the macros one, and a map's in the
|
|
388
|
-
// scenes one.
|
|
389
|
-
const { value: authoredFolder, isAddress } = folderField(fm);
|
|
384
|
+
// sits under the same heading a reader found the item under.
|
|
385
|
+
//
|
|
390
386
|
// An address is resolved wherever it is written, including here — and
|
|
391
387
|
// resolving it *here* is what cures the defect this comment used to
|
|
392
388
|
// describe. A folder note has one definition and one address, so the
|
|
@@ -395,12 +391,10 @@ export class Journals extends BasePackCompiler {
|
|
|
395
391
|
// with the first, and so no arrangement to assume: the mirroring
|
|
396
392
|
// failure is unrepresentable rather than merely reported.
|
|
397
393
|
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
: ownsDoc ? authoredFolder
|
|
403
|
-
: this.folderResolver(authoredFolder);
|
|
394
|
+
// The id spelling used to cross packs verbatim here, on the assumption
|
|
395
|
+
// both declared it — the arrangement #260 retires with the YAML.
|
|
396
|
+
const { value: authoredFolder } = folderField(fm);
|
|
397
|
+
const folder = this.folderResolver(authoredFolder, { isAddress: true });
|
|
404
398
|
|
|
405
399
|
return buildJournalEntry({
|
|
406
400
|
id,
|
package/engine/scenes.mjs
CHANGED
|
@@ -408,8 +408,8 @@ export class Scenes extends BasePackCompiler {
|
|
|
408
408
|
// shared `docEntryTypes` arrangement (#1514) — so neither
|
|
409
409
|
// pass has to read the other's output.
|
|
410
410
|
const entryId = hasBody ? itemDocEntryId(fm.id) : undefined;
|
|
411
|
-
const { value: authoredFolder
|
|
412
|
-
const folder = this.folderResolver(authoredFolder, { isAddress:
|
|
411
|
+
const { value: authoredFolder } = folderField(fm);
|
|
412
|
+
const folder = this.folderResolver(authoredFolder, { isAddress: true });
|
|
413
413
|
// The retired spelling of the background art, reported where an author
|
|
414
414
|
// meets it soonest — every consumer runs the compile, and not every
|
|
415
415
|
// one runs the lint (#142). Located by reading the note back, which is
|
|
@@ -457,13 +457,11 @@ export class Scenes extends BasePackCompiler {
|
|
|
457
457
|
name,
|
|
458
458
|
markdown,
|
|
459
459
|
leadName: name,
|
|
460
|
-
// As in the journals pass: an
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
this.folderResolver(authoredFolder, { isAddress: true })
|
|
466
|
-
: authoredFolder,
|
|
460
|
+
// As in the journals pass: an address resolves in the
|
|
461
|
+
// pack that emits it, which is what makes the folder
|
|
462
|
+
// materialise there too (#257). The id spelling that used
|
|
463
|
+
// to cross packs verbatim is retired (#260).
|
|
464
|
+
folder: this.folderResolver(authoredFolder, { isAddress: true }),
|
|
467
465
|
flags: fm.flags,
|
|
468
466
|
})
|
|
469
467
|
: null;
|