@heroiclands/package-build 8.0.0 → 9.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/bin/content-build.mjs +44 -118
  3. package/bin/package-build.mjs +27 -69
  4. package/bin/report.mjs +1 -2
  5. package/bundle.mjs +2 -10
  6. package/config.mjs +31 -106
  7. package/container.mjs +13 -57
  8. package/content-config.mjs +45 -164
  9. package/coverage.mjs +14 -55
  10. package/deploy.mjs +4 -13
  11. package/e2e.mjs +16 -55
  12. package/engine/address-diff.mjs +1 -4
  13. package/engine/base-compiler.mjs +10 -28
  14. package/engine/code-fences.mjs +4 -13
  15. package/engine/compendiums.mjs +13 -37
  16. package/engine/content-address.mjs +2 -6
  17. package/engine/content-links.mjs +13 -44
  18. package/engine/content-lint.mjs +4 -15
  19. package/engine/content-slug.mjs +2 -6
  20. package/engine/content-tables.mjs +26 -79
  21. package/engine/diagnostics.mjs +4 -15
  22. package/engine/field-reference.mjs +6 -20
  23. package/engine/field-spec.mjs +1 -3
  24. package/engine/foreign-catalog.mjs +7 -22
  25. package/engine/foreign-manifests.mjs +1 -4
  26. package/engine/frontmatter-lint.mjs +6 -18
  27. package/engine/frontmatter.mjs +3 -8
  28. package/engine/generate.mjs +4 -16
  29. package/engine/helpers.mjs +13 -52
  30. package/engine/homepage.mjs +4 -15
  31. package/engine/ids.mjs +3 -12
  32. package/engine/item-registry.mjs +2 -6
  33. package/engine/journals.mjs +4 -14
  34. package/engine/kb-manifest.mjs +5 -17
  35. package/engine/macros.mjs +2 -10
  36. package/engine/manifest-emit.mjs +6 -17
  37. package/engine/map-notes.mjs +19 -69
  38. package/engine/note-package.mjs +1 -4
  39. package/engine/pack-config.mjs +17 -38
  40. package/engine/pack-router.mjs +1 -2
  41. package/engine/prose-config.mjs +20 -4
  42. package/engine/prose-lint.mjs +6 -14
  43. package/engine/region-events.mjs +1 -3
  44. package/engine/scene-levels.mjs +8 -22
  45. package/engine/scenes.mjs +13 -47
  46. package/engine/schema-check.mjs +1 -4
  47. package/engine/schema-extract.mjs +70 -45
  48. package/engine/site-build.mjs +12 -37
  49. package/engine/site-index.mjs +3 -15
  50. package/engine/web-wikilinks.mjs +4 -13
  51. package/engine/wikilinks.mjs +115 -167
  52. package/index.mjs +1 -5
  53. package/lang.mjs +1 -3
  54. package/manifest.mjs +10 -37
  55. package/markdownlint-config.mjs +1 -5
  56. package/package.json +1 -1
  57. package/sohl/actors.mjs +10 -40
  58. package/sohl/being-info.mjs +3 -6
  59. package/sohl/index.mjs +1 -6
  60. package/sohl/item-builders.mjs +1 -3
  61. package/sohl/item-fields.mjs +16 -34
  62. package/sohl/items.mjs +1 -3
  63. package/sohl/kb-passes.mjs +29 -39
  64. package/sohl/skill-base.mjs +7 -23
  65. package/stage.mjs +3 -13
  66. package/templates.mjs +4 -15
  67. package/types/bundle.d.mts +1 -1
  68. package/types/container.d.mts +2 -2
  69. package/types/coverage.d.mts +1 -1
  70. package/types/e2e.d.mts +4 -4
  71. package/types/engine/generate.d.mts +1 -1
  72. package/types/engine/helpers.d.mts +1 -1
  73. package/types/engine/schema-extract.d.mts +1 -1
  74. package/types/engine/site-index.d.mts +1 -1
  75. package/types/manifest.d.mts +1 -1
@@ -82,13 +82,7 @@
82
82
 
83
83
  import crypto from "crypto";
84
84
 
85
- import {
86
- compendiumUuid,
87
- ITEM_PACK,
88
- packForType,
89
- pageUuid,
90
- PACK_BY_TYPE,
91
- } from "./ids.mjs";
85
+ import { compendiumUuid, ITEM_PACK, packForType, pageUuid, PACK_BY_TYPE } from "./ids.mjs";
92
86
  import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
93
87
  import { replaceOutsideCode } from "./code-fences.mjs";
94
88
  // The syntax lives in `./wikilink-syntax.mjs`, so the web resolver and this
@@ -194,11 +188,7 @@ export function readQualifier(target, types, packages) {
194
188
 
195
189
  const slash = target.lastIndexOf("/");
196
190
  if (slash > 0) {
197
- const read = readTypeAndCode(
198
- target.slice(0, slash),
199
- target.slice(slash + 1),
200
- types,
201
- );
191
+ const read = readTypeAndCode(target.slice(0, slash), target.slice(slash + 1), types);
202
192
  // A slash means qualified whether or not the type is real.
203
193
  return read ?? { reason: "unknown-type" };
204
194
  }
@@ -206,11 +196,7 @@ export function readQualifier(target, types, packages) {
206
196
  const hyphen = target.indexOf("-");
207
197
  if (hyphen > 0) {
208
198
  // A hyphen qualifies only on a known type; otherwise it is part of a name.
209
- return readTypeAndCode(
210
- target.slice(0, hyphen),
211
- target.slice(hyphen + 1),
212
- types,
213
- );
199
+ return readTypeAndCode(target.slice(0, hyphen), target.slice(hyphen + 1), types);
214
200
  }
215
201
  return null;
216
202
  }
@@ -313,23 +299,14 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage) {
313
299
  // An item's prose compiles into a separate JournalEntry, addressed
314
300
  // by the virtual `doc<type>` qualifier. Its id is derived from the
315
301
  // item's, so its address is knowable here too.
316
- docUuid: compendiumUuid(
317
- packageId,
318
- "doc",
319
- itemDocEntryId(d.id),
320
- d.docPack,
321
- ),
302
+ docUuid: compendiumUuid(packageId, "doc", itemDocEntryId(d.id), d.docPack),
322
303
  });
323
304
 
324
- if (d.shortcode)
325
- byShortcode.set(`${norm(d.type)}/${norm(d.shortcode)}`, d);
305
+ if (d.shortcode) byShortcode.set(`${norm(d.type)}/${norm(d.shortcode)}`, d);
326
306
  for (const a of d.aliases ?? []) {
327
307
  const key = `${norm(d.type)}|${norm(a)}`;
328
308
  // Second claimant poisons the alias: it can no longer be resolved.
329
- byAlias.set(
330
- key,
331
- byAlias.has(key) && byAlias.get(key) !== d ? null : d,
332
- );
309
+ byAlias.set(key, byAlias.has(key) && byAlias.get(key) !== d ? null : d);
333
310
  // Every claimant is kept alongside, because poisoning the alias
334
311
  // discards exactly the information needed to report the problem.
335
312
  // The note that *cites* an ambiguous alias is innocent — whoever
@@ -411,11 +388,7 @@ function findForeign(index, read) {
411
388
  const wanted = norm(read.itemDoc ? `doc${read.type}` : read.type);
412
389
  const shortcode = norm(read.shortcode);
413
390
  if (read.package) {
414
- return (
415
- index.foreign.get(
416
- `${read.package}-${wanted}-${shortcode}`.toLowerCase(),
417
- ) ?? null
418
- );
391
+ return index.foreign.get(`${read.package}-${wanted}-${shortcode}`.toLowerCase()) ?? null;
419
392
  }
420
393
  const hits = [];
421
394
  for (const [key, v] of index.foreign) {
@@ -490,159 +463,134 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
490
463
  // `offset` is the third replacer argument because the pattern has exactly
491
464
  // one capture group. It is what makes two identical unresolved links on
492
465
  // one note tellable apart, and a position reportable at all (#17).
493
- const out = replaceOutsideCode(
494
- markdown,
495
- WIKILINK,
496
- (all, rawInner, offset) => {
497
- const parsed = parseWikilink(rawInner);
498
- const { labelled } = parsed;
499
- let target = parsed.target;
500
- // An unlabelled link shows its interior verbatim, anchor included;
501
- // a labelled one shows its label. An *empty* label is not a label
502
- // `[[x|]]` means "show the target's name" and that reading
503
- // comes from {@link authoredLabel} so the web resolver cannot draw
504
- // the line somewhere else (#113).
505
- let text = labelled ? (authoredLabel(parsed) ?? "") : parsed.inner;
506
- const slug = parsed.anchor || null;
466
+ const out = replaceOutsideCode(markdown, WIKILINK, (all, rawInner, offset) => {
467
+ const parsed = parseWikilink(rawInner);
468
+ const { labelled } = parsed;
469
+ let target = parsed.target;
470
+ // An unlabelled link shows its interior verbatim, anchor included;
471
+ // a labelled one shows its label. An *empty* label is not a label
472
+ // — `[[x|]]` means "show the target's name" — and that reading
473
+ // comes from {@link authoredLabel} so the web resolver cannot draw
474
+ // the line somewhere else (#113).
475
+ let text = labelled ? (authoredLabel(parsed) ?? "") : parsed.inner;
476
+ const slug = parsed.anchor || null;
507
477
 
508
- // Resolve the document: same-page (empty target), type-shortcode, or alias.
509
- let doc;
510
- // Set when the qualifier was the virtual `doc<type>` form, so the UUID
511
- // is built against the item doc entry rather than the item itself.
512
- let itemDoc = false;
513
- // Set when the target was read as `type-shortcode` — an address rather
514
- // than prose, which decides what an unlabelled link shows (#1409).
515
- let addressed = false;
516
- // Kept for the foreign fallback below, which needs the parsed address.
517
- let qualifiedRead = null;
518
- if (target === "" && slug) {
519
- doc = { type, id, pack, docPack };
520
- } else {
521
- const qualified = readQualifier(
478
+ // Resolve the document: same-page (empty target), type-shortcode, or alias.
479
+ let doc;
480
+ // Set when the qualifier was the virtual `doc<type>` form, so the UUID
481
+ // is built against the item doc entry rather than the item itself.
482
+ let itemDoc = false;
483
+ // Set when the target was read as `type-shortcode` — an address rather
484
+ // than prose, which decides what an unlabelled link shows (#1409).
485
+ let addressed = false;
486
+ // Kept for the foreign fallback below, which needs the parsed address.
487
+ let qualifiedRead = null;
488
+ if (target === "" && slug) {
489
+ doc = { type, id, pack, docPack };
490
+ } else {
491
+ const qualified = readQualifier(target, index.types, index.packages);
492
+ qualifiedRead = qualified;
493
+ if (qualified?.reason) {
494
+ unresolved.push({
495
+ link: all,
522
496
  target,
523
- index.types,
524
- index.packages,
525
- );
526
- qualifiedRead = qualified;
527
- if (qualified?.reason) {
497
+ offset,
498
+ reason: qualified.reason,
499
+ });
500
+ return unresolvedLink(text || target, target);
501
+ }
502
+ if (qualified) {
503
+ addressed = true;
504
+ itemDoc = qualified.itemDoc;
505
+ doc = index.byShortcode.get(`${qualified.type}/${qualified.shortcode}`);
506
+ } else {
507
+ const aliasKey = `${norm(type)}|${norm(target)}`;
508
+ const hit = index.byAlias.get(aliasKey);
509
+ if (hit === null) {
528
510
  unresolved.push({
529
511
  link: all,
530
512
  target,
531
513
  offset,
532
- reason: qualified.reason,
514
+ reason: "ambiguous",
515
+ // Who claimed it, so the report can name the collision
516
+ // rather than the note that merely cites it (#13).
517
+ candidates: (index.aliasClaims?.get(aliasKey) ?? []).map((d) => ({
518
+ type: d.type,
519
+ shortcode: d.shortcode,
520
+ name: d.name,
521
+ })),
533
522
  });
534
523
  return unresolvedLink(text || target, target);
535
524
  }
536
- if (qualified) {
537
- addressed = true;
538
- itemDoc = qualified.itemDoc;
539
- doc = index.byShortcode.get(
540
- `${qualified.type}/${qualified.shortcode}`,
541
- );
542
- } else {
543
- const aliasKey = `${norm(type)}|${norm(target)}`;
544
- const hit = index.byAlias.get(aliasKey);
545
- if (hit === null) {
546
- unresolved.push({
547
- link: all,
548
- target,
549
- offset,
550
- reason: "ambiguous",
551
- // Who claimed it, so the report can name the collision
552
- // rather than the note that merely cites it (#13).
553
- candidates: (
554
- index.aliasClaims?.get(aliasKey) ?? []
555
- ).map((d) => ({
556
- type: d.type,
557
- shortcode: d.shortcode,
558
- name: d.name,
559
- })),
560
- });
561
- return unresolvedLink(text || target, target);
562
- }
563
- doc = hit;
564
- }
525
+ doc = hit;
565
526
  }
566
- if (!doc) {
567
- // Nothing local answers. A foreign package may publish this
568
- // address, in which case the manifest hands back a complete UUID —
569
- // including, for a section link, the anchor's own so nothing is
570
- // derived here.
571
- const hit = findForeign(index, qualifiedRead);
572
- if (hit) {
573
- const uuid = slug ? hit.anchors?.[slug] : hit.uuid;
574
- if (uuid) {
575
- return `@UUID[${uuid}]{${text || hit.name || target}}`;
576
- }
577
- unresolved.push({
578
- link: all,
579
- target,
580
- offset,
581
- reason: "unknown-anchor",
582
- addressed: true,
583
- });
584
- return unresolvedLink(text || target, target);
527
+ }
528
+ if (!doc) {
529
+ // Nothing local answers. A foreign package may publish this
530
+ // address, in which case the manifest hands back a complete UUID —
531
+ // including, for a section link, the anchor's own — so nothing is
532
+ // derived here.
533
+ const hit = findForeign(index, qualifiedRead);
534
+ if (hit) {
535
+ const uuid = slug ? hit.anchors?.[slug] : hit.uuid;
536
+ if (uuid) {
537
+ return `@UUID[${uuid}]{${text || hit.name || target}}`;
585
538
  }
586
539
  unresolved.push({
587
540
  link: all,
588
541
  target,
589
542
  offset,
590
- reason: "unknown",
591
- // A *qualified* address that resolves nowhere is a typo: every
592
- // package it could name is either built here or vendored, so
593
- // there is no third possibility left. A bare alias is not — it
594
- // may simply be prose.
595
- addressed: !!qualifiedRead && !qualifiedRead.reason,
543
+ reason: "unknown-anchor",
544
+ addressed: true,
596
545
  });
597
546
  return unresolvedLink(text || target, target);
598
547
  }
548
+ unresolved.push({
549
+ link: all,
550
+ target,
551
+ offset,
552
+ reason: "unknown",
553
+ // A *qualified* address that resolves nowhere is a typo: every
554
+ // package it could name is either built here or vendored, so
555
+ // there is no third possibility left. A bare alias is not — it
556
+ // may simply be prose.
557
+ addressed: !!qualifiedRead && !qualifiedRead.reason,
558
+ });
559
+ return unresolvedLink(text || target, target);
560
+ }
599
561
 
600
- // With no explicit label, a *qualified* target has no prose to show — a
601
- // shortcode is an address, not display text — so the document's own name
602
- // stands in (#1409). A bare `[[Text]]` is already the prose the author
603
- // wrote, and substituting the canonical name there would rewrite the
604
- // sentence ("worsens the [[Shock State]]" must not render as "Shock").
605
- // The knowledgebase build reads the same authored link the same way.
606
- if (!text || (!labelled && addressed)) text = doc.name ?? target;
562
+ // With no explicit label, a *qualified* target has no prose to show — a
563
+ // shortcode is an address, not display text — so the document's own name
564
+ // stands in (#1409). A bare `[[Text]]` is already the prose the author
565
+ // wrote, and substituting the canonical name there would rewrite the
566
+ // sentence ("worsens the [[Shock State]]" must not render as "Shock").
567
+ // The knowledgebase build reads the same authored link the same way.
568
+ if (!text || (!labelled && addressed)) text = doc.name ?? target;
607
569
 
608
- // Both addresses were computed when the target was indexed. An item
609
- // doc lives in the journals pack under its own derived entry id, and
610
- // its pages hash against *that* id — not the item's.
611
- //
612
- // The one target with no index entry is the note itself: a `[[#slug]]`
613
- // self-link is resolved from the source's own type and id, which the
614
- // caller supplied, so it is addressed the same way here.
615
- const addresses = index.uuidByDoc.get(doc) ?? {
616
- uuid: compendiumUuid(
617
- index.packageId,
618
- doc.type,
619
- doc.id,
620
- doc.pack,
621
- ),
622
- docUuid: compendiumUuid(
623
- index.packageId,
624
- "doc",
625
- itemDocEntryId(doc.id),
626
- doc.docPack,
627
- ),
628
- };
629
- const entryUuid = itemDoc ? addresses.docUuid : addresses.uuid;
630
- const entryId = itemDoc ? itemDocEntryId(doc.id) : doc.id;
631
- const isJournal =
632
- itemDoc || packForType(doc.type).docType === "JournalEntry";
633
- // A JournalEntry link opens a journal — at its first page, or at the
634
- // page an anchor names. An Item or Actor link opens that document's
635
- // *sheet*, which has no sections, so the anchor has nothing to address
636
- // and is dropped. Forging a JournalEntryPage id onto a document that
637
- // can never hold one is what made such links dead-end (#1362); an
638
- // item's pages are addressed through its `doc<type>` counterpart.
639
- const uuid =
640
- slug && isJournal ?
641
- pageUuid(entryUuid, anchorPageId(entryId, slug))
642
- : entryUuid;
643
- return `@UUID[${uuid}]{${text}}`;
644
- },
645
- );
570
+ // Both addresses were computed when the target was indexed. An item
571
+ // doc lives in the journals pack under its own derived entry id, and
572
+ // its pages hash against *that* id — not the item's.
573
+ //
574
+ // The one target with no index entry is the note itself: a `[[#slug]]`
575
+ // self-link is resolved from the source's own type and id, which the
576
+ // caller supplied, so it is addressed the same way here.
577
+ const addresses = index.uuidByDoc.get(doc) ?? {
578
+ uuid: compendiumUuid(index.packageId, doc.type, doc.id, doc.pack),
579
+ docUuid: compendiumUuid(index.packageId, "doc", itemDocEntryId(doc.id), doc.docPack),
580
+ };
581
+ const entryUuid = itemDoc ? addresses.docUuid : addresses.uuid;
582
+ const entryId = itemDoc ? itemDocEntryId(doc.id) : doc.id;
583
+ const isJournal = itemDoc || packForType(doc.type).docType === "JournalEntry";
584
+ // A JournalEntry link opens a journal — at its first page, or at the
585
+ // page an anchor names. An Item or Actor link opens that document's
586
+ // *sheet*, which has no sections, so the anchor has nothing to address
587
+ // and is dropped. Forging a JournalEntryPage id onto a document that
588
+ // can never hold one is what made such links dead-end (#1362); an
589
+ // item's pages are addressed through its `doc<type>` counterpart.
590
+ const uuid =
591
+ slug && isJournal ? pageUuid(entryUuid, anchorPageId(entryId, slug)) : entryUuid;
592
+ return `@UUID[${uuid}]{${text}}`;
593
+ });
646
594
 
647
595
  return { markdown: out, unresolved };
648
596
  }
package/index.mjs CHANGED
@@ -42,11 +42,7 @@
42
42
  // ── Content: notes to compendium packs, site content, link manifests ────────
43
43
 
44
44
  /** The configuration contract a consuming repository declares its build with. */
45
- export {
46
- defineConfig,
47
- PACKAGE_KINDS,
48
- PACK_DOCUMENT_TYPES,
49
- } from "./content-config.mjs";
45
+ export { defineConfig, PACKAGE_KINDS, PACK_DOCUMENT_TYPES } from "./content-config.mjs";
50
46
 
51
47
  /** The content pipeline — walking, compiling, linking, emitting. */
52
48
  export * as engine from "./engine/index.mjs";
package/lang.mjs CHANGED
@@ -116,9 +116,7 @@ export function validateLangSource(raw) {
116
116
  } catch (err) {
117
117
  // Nothing further can be said about a file that does not parse, and
118
118
  // guessing at its intended shape would only bury this finding.
119
- return [
120
- { severity: "error", message: `not valid JSON: ${err.message}` },
121
- ];
119
+ return [{ severity: "error", message: `not valid JSON: ${err.message}` }];
122
120
  }
123
121
 
124
122
  if (!isRecord(json)) {
package/manifest.mjs CHANGED
@@ -81,8 +81,7 @@ export const ARTIFACTS = Object.freeze(["system", "module"]);
81
81
  * worse than a missing one: Foundry installs it and never offers an update.
82
82
  */
83
83
  export function normalizeRepoUrl(repository) {
84
- const raw =
85
- typeof repository === "string" ? repository : (repository?.url ?? "");
84
+ const raw = typeof repository === "string" ? repository : (repository?.url ?? "");
86
85
  const url = String(raw)
87
86
  .trim()
88
87
  .replace(/^git\+/, "")
@@ -177,10 +176,7 @@ const MANIFEST_KEY_ORDER = Object.freeze([
177
176
  * @returns {object[]} The manifest's `packs` array.
178
177
  */
179
178
  export function manifestPacks(config) {
180
- const flatten = (pack) => [
181
- pack,
182
- ...(pack.companions ?? []).flatMap(flatten),
183
- ];
179
+ const flatten = (pack) => [pack, ...(pack.companions ?? []).flatMap(flatten)];
184
180
  return config.packs.flatMap(flatten).map((pack) => {
185
181
  // Foundry requires `system` on ActiveEffect, Actor and Item packs and
186
182
  // on no others, so the value is per pack: its own declaration first,
@@ -205,11 +201,7 @@ export function manifestPacks(config) {
205
201
  *
206
202
  * @type {readonly string[]}
207
203
  */
208
- const PACK_FOLDERS_PATH = Object.freeze([
209
- "packageBuild",
210
- "manifest",
211
- "packFolders",
212
- ]);
204
+ const PACK_FOLDERS_PATH = Object.freeze(["packageBuild", "manifest", "packFolders"]);
213
205
 
214
206
  /**
215
207
  * Every pack name a folder tree names, with the folder that named it.
@@ -371,8 +363,7 @@ export const BUILD_ONLY_RELATIONSHIP_KEYS = Object.freeze(["itemCatalog"]);
371
363
  export function publishedRelationships(relationships) {
372
364
  const published = {};
373
365
  for (const [kind, entries] of Object.entries(relationships)) {
374
- published[kind] =
375
- Array.isArray(entries) ? entries.map(withoutBuildKeys) : entries;
366
+ published[kind] = Array.isArray(entries) ? entries.map(withoutBuildKeys) : entries;
376
367
  }
377
368
  return published;
378
369
  }
@@ -388,9 +379,7 @@ function withoutBuildKeys(entry) {
388
379
  return entry;
389
380
  }
390
381
  return Object.fromEntries(
391
- Object.entries(entry).filter(
392
- ([key]) => !BUILD_ONLY_RELATIONSHIP_KEYS.includes(key),
393
- ),
382
+ Object.entries(entry).filter(([key]) => !BUILD_ONLY_RELATIONSHIP_KEYS.includes(key)),
394
383
  );
395
384
  }
396
385
 
@@ -447,23 +436,18 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
447
436
  const entry = {
448
437
  id: config.requiresSystem,
449
438
  type: "system",
450
- ...(declaredSystem?.manifest ?
451
- { manifest: declaredSystem.manifest }
452
- : {}),
439
+ ...(declaredSystem?.manifest ? { manifest: declaredSystem.manifest } : {}),
453
440
  ...(declaredSystem?.compatibility ?
454
441
  {
455
442
  compatibility: Object.fromEntries(
456
- Object.entries(declaredSystem.compatibility).filter(
457
- ([, v]) => v != null,
458
- ),
443
+ Object.entries(declaredSystem.compatibility).filter(([, v]) => v != null),
459
444
  ),
460
445
  }
461
446
  : {}),
462
447
  };
463
448
  // An explicit `relationships.systems` still wins, so a repository
464
449
  // mid-migration is never told two different things about itself.
465
- relationships.systems =
466
- relationships.systems?.length ? relationships.systems : [entry];
450
+ relationships.systems = relationships.systems?.length ? relationships.systems : [entry];
467
451
  }
468
452
  if (Object.keys(relationships).length) {
469
453
  derived.relationships = publishedRelationships(relationships);
@@ -553,14 +537,7 @@ function reportPackFolders(findings, configFile) {
553
537
  * @throws {Error} When a `packFolders` entry names a pack the package does not
554
538
  * ship. Nothing is written in that case.
555
539
  */
556
- export async function writeManifest({
557
- config,
558
- packageJson,
559
- artifact,
560
- outDir,
561
- flags,
562
- configFile,
563
- }) {
540
+ export async function writeManifest({ config, packageJson, artifact, outDir, flags, configFile }) {
564
541
  const manifest = buildManifest({ config, packageJson, artifact, flags });
565
542
 
566
543
  const errors = reportPackFolders(
@@ -583,10 +560,6 @@ export async function writeManifest({
583
560
  const outPath = path.join(outDir, `${artifact}.json`);
584
561
  // Trailing newline: the file is committed to a release archive and read by
585
562
  // humans as often as by Foundry.
586
- await fs.writeFile(
587
- outPath,
588
- `${JSON.stringify(manifest, null, 2)}\n`,
589
- "utf8",
590
- );
563
+ await fs.writeFile(outPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
591
564
  return { path: outPath, manifest };
592
565
  }
@@ -31,11 +31,7 @@
31
31
  * @module
32
32
  */
33
33
 
34
- import {
35
- MARKDOWNLINT_CONFIG,
36
- MARKDOWN_GLOBS,
37
- MARKDOWN_IGNORES,
38
- } from "./engine/prose-config.mjs";
34
+ import { MARKDOWNLINT_CONFIG, MARKDOWN_GLOBS, MARKDOWN_IGNORES } from "./engine/prose-config.mjs";
39
35
 
40
36
  export default {
41
37
  config: MARKDOWNLINT_CONFIG,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "8.0.0",
3
+ "version": "9.0.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
package/sohl/actors.mjs CHANGED
@@ -151,9 +151,7 @@ function extractBodyAndMovement(fm) {
151
151
  return {
152
152
  body: normalizeBody(sohlField(fm, "body", {})),
153
153
  currentMoveMedium: String(sohlField(fm, "currentMoveMedium", "none")),
154
- movementProfiles: normalizeMovementProfiles(
155
- sohlField(fm, "movementProfiles", []),
156
- ),
154
+ movementProfiles: normalizeMovementProfiles(sohlField(fm, "movementProfiles", [])),
157
155
  };
158
156
  }
159
157
 
@@ -290,8 +288,7 @@ function extractAnchorSection(body, anchorId) {
290
288
  if (capturing) captured.push(line);
291
289
  continue;
292
290
  }
293
- const h1Match =
294
- !inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
291
+ const h1Match = !inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
295
292
  if (h1Match) {
296
293
  const anchor = h1Match[1].match(/\{#([^}]+)\}\s*$/);
297
294
  const id = anchor?.[1]?.trim().toLowerCase() || null;
@@ -383,13 +380,8 @@ export class Actors extends BasePackCompiler {
383
380
  */
384
381
  async prepare() {
385
382
  await super.prepare();
386
- this.itemsMap = loadItemsMap(
387
- this.itemsSourceDirs,
388
- this.foreignSourceDirs,
389
- );
390
- log.info(
391
- `Loaded ${this.itemsMap.size} predefined items for actor resolution`,
392
- );
383
+ this.itemsMap = loadItemsMap(this.itemsSourceDirs, this.foreignSourceDirs);
384
+ log.info(`Loaded ${this.itemsMap.size} predefined items for actor resolution`);
393
385
  }
394
386
 
395
387
  /**
@@ -421,22 +413,12 @@ export class Actors extends BasePackCompiler {
421
413
  * `(actorId, type, shortcode, indexKey)` so re-exports are stable.
422
414
  * Returns null if the descriptor cannot be resolved.
423
415
  */
424
- resolveEmbedded(
425
- itemsMap,
426
- actorId,
427
- type,
428
- shortcode,
429
- overlay,
430
- indexKey,
431
- ctx,
432
- ) {
416
+ resolveEmbedded(itemsMap, actorId, type, shortcode, overlay, indexKey, ctx) {
433
417
  let base = null;
434
418
  if (shortcode) {
435
419
  base = itemsMap.get(`${type}:${shortcode}`);
436
420
  if (!base) {
437
- this.noteError(
438
- `${ctx}: no predefined item for "${type}:${shortcode}"`,
439
- );
421
+ this.noteError(`${ctx}: no predefined item for "${type}:${shortcode}"`);
440
422
  this.errorCount++;
441
423
  return null;
442
424
  }
@@ -452,10 +434,7 @@ export class Actors extends BasePackCompiler {
452
434
  }
453
435
  const merged = overlay ? deepMerge(base, overlay) : base;
454
436
  merged.type = type;
455
- merged._id = makeId(
456
- actorId,
457
- `${type}:${shortcode || merged.name}:${indexKey}`,
458
- );
437
+ merged._id = makeId(actorId, `${type}:${shortcode || merged.name}:${indexKey}`);
459
438
  // Foundry's pack compiler flattens the document hierarchy into LevelDB,
460
439
  // storing each embedded document under its own `_key`. Embedded items
461
440
  // therefore need a hierarchical key, as do any effects they carry
@@ -499,9 +478,7 @@ export class Actors extends BasePackCompiler {
499
478
  if (Array.isArray(sohlItems)) {
500
479
  sohlItems.forEach((entry, index) => {
501
480
  if (!entry || typeof entry !== "object") {
502
- this.noteError(
503
- `${ctx}: sohl.items[${index}] is not an object`,
504
- );
481
+ this.noteError(`${ctx}: sohl.items[${index}] is not an object`);
505
482
  this.errorCount++;
506
483
  return;
507
484
  }
@@ -556,10 +533,7 @@ export class Actors extends BasePackCompiler {
556
533
  */
557
534
  openUnopenedSkills(items, ctx) {
558
535
  const skills = items.filter(
559
- (item) =>
560
- item.type === "skill" &&
561
- item.system &&
562
- item.system.masteryLevelBase == null,
536
+ (item) => item.type === "skill" && item.system && item.system.masteryLevelBase == null,
563
537
  );
564
538
  if (!skills.length) return;
565
539
 
@@ -621,11 +595,7 @@ export class Actors extends BasePackCompiler {
621
595
  }
622
596
 
623
597
  // Being-only combat grouping (mirrors `system.defaultCombatGroup`).
624
- const defaultCombatGroup = sohlField(
625
- fm,
626
- "defaultCombatGroup",
627
- undefined,
628
- );
598
+ const defaultCombatGroup = sohlField(fm, "defaultCombatGroup", undefined);
629
599
  if (defaultCombatGroup !== undefined) {
630
600
  system.defaultCombatGroup = defaultCombatGroup;
631
601
  }
@@ -108,8 +108,7 @@ export function deriveBeingInfo(sohl, index) {
108
108
  const items = Array.isArray(out.items) ? out.items : [];
109
109
  if (items.length === 0) return out;
110
110
 
111
- const lookup = (type, shortcode) =>
112
- shortcode ? index.get(`${type}:${shortcode}`) : undefined;
111
+ const lookup = (type, shortcode) => (shortcode ? index.get(`${type}:${shortcode}`) : undefined);
113
112
 
114
113
  /** An item's display name: its own, then the index's, then its shortcode. */
115
114
  const displayName = (it, ref, shortcode) =>
@@ -135,8 +134,7 @@ export function deriveBeingInfo(sohl, index) {
135
134
  if (!isMap(it)) continue;
136
135
  const key = GEAR_TYPE_TO_KEY[it.type];
137
136
  if (!key) continue;
138
- const shortcode =
139
- typeof it.shortcode === "string" ? it.shortcode : undefined;
137
+ const shortcode = typeof it.shortcode === "string" ? it.shortcode : undefined;
140
138
  const ref = lookup(it.type, shortcode);
141
139
  const name = displayName(it, ref, shortcode);
142
140
  if (!name) continue;
@@ -153,8 +151,7 @@ export function deriveBeingInfo(sohl, index) {
153
151
  const talents = [];
154
152
  for (const it of items) {
155
153
  if (!isMap(it) || it.type !== "mysticalability") continue;
156
- const shortcode =
157
- typeof it.shortcode === "string" ? it.shortcode : undefined;
154
+ const shortcode = typeof it.shortcode === "string" ? it.shortcode : undefined;
158
155
  const ref = lookup("mysticalability", shortcode);
159
156
  // No shortcode fallback here: an ability with neither an inline name
160
157
  // nor an index entry has nothing to show, and a row reading like a
package/sohl/index.mjs CHANGED
@@ -47,9 +47,4 @@ export * as kbPasses from "./kb-passes.mjs";
47
47
  // compilers arrived (#1510).
48
48
  export { DEFAULT_ITEM_ART, defaultItemArt } from "./default-item-art.mjs";
49
49
  export { AFFILIATION_STANDINGS } from "./affiliation-standings.mjs";
50
- export {
51
- BEING_TYPE,
52
- GEAR_TYPE_TO_KEY,
53
- deriveBeingInfo,
54
- isBeing,
55
- } from "./being-info.mjs";
50
+ export { BEING_TYPE, GEAR_TYPE_TO_KEY, deriveBeingInfo, isBeing } from "./being-info.mjs";