@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.
@@ -27,7 +27,7 @@
27
27
  * itemBuilders: sohl
28
28
  * skipDirectories: [Templates]
29
29
  * packs:
30
- * - { name: items, type: Item, folders: item-folders.yaml }
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 `folders` file that can never be
897
- // read is worse than refusing the configuration that declares it.
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
@@ -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. Two spellings are read, and
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 `folder`.** It is a note's _pack_ folder, not its directory — the
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
  ---
@@ -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 });
@@ -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(["folder", "packFolder", "pack", "archetype", "templatePriority", "kbcat"]),
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
@@ -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: sohlField(fm, "folder", null), isAddress: false };
332
+ return { value: null, isAddress: true };
333
333
  }
@@ -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, folders, companions, system },
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 which is why these
396
- // documents are written after `compile()` and the YAML ones before it
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 address or by id.
384
+ * The Foundry folder id a note names, by its folder note's address.
403
385
  *
404
- * The two spellings resolve against two different sources and always did:
405
- * `packFolder` names a folder **note**, resolved through the address index
406
- * shared by the whole build, and `folder` names a Foundry **id** declared
407
- * in this pack's own YAML. Which one applies is the field the value was
408
- * written in, never the string (#251).
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, { isAddress = false } = {}) => {
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,
@@ -913,102 +913,9 @@ export function expandNoteTables(body, { docs, name, fm, bodyLine, sqlTables })
913
913
  }
914
914
 
915
915
  /* ------------------------------------------------------------------------ */
916
- /* Folder hierarchy: loading, resolution, emission */
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
- }
@@ -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, so both
300
- // spellings work wherever a note already writes one. `packFolder` is a
301
- // folder note's address and `folder` an id; which it is comes from the
302
- // field, never from the string (#251, #255).
303
- const packFolderAddress = blockField(fm, system, "packFolder", null);
304
- const folder =
305
- packFolderAddress ?
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,
@@ -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. The id is
385
- // taken verbatim rather than through `folderResolver`, which validates
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
- // `folder:` is unchanged, and still crosses packs verbatim on the
399
- // assumption both declare it — the arrangement #260 retires.
400
- const folder =
401
- isAddress ? this.folderResolver(authoredFolder, { isAddress: true })
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, isAddress: folderIsAddress } = folderField(fm);
412
- const folder = this.folderResolver(authoredFolder, { isAddress: folderIsAddress });
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 id crosses packs verbatim,
461
- // an address resolves in the pack that emits it which is
462
- // what makes the folder materialise there too (#257).
463
- folder:
464
- folderIsAddress ?
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;