@heroiclands/package-build 8.1.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +479 -123
  4. package/bin/package-build.mjs +27 -69
  5. package/bin/report.mjs +1 -2
  6. package/bundle.mjs +2 -10
  7. package/config.mjs +31 -106
  8. package/container.mjs +13 -57
  9. package/content-config.mjs +300 -188
  10. package/coverage.mjs +14 -55
  11. package/deploy.mjs +4 -13
  12. package/docs/content-format.md +1418 -0
  13. package/e2e.mjs +16 -55
  14. package/engine/address-charset.mjs +62 -0
  15. package/engine/address-diff.mjs +1 -4
  16. package/engine/alias-index.mjs +153 -0
  17. package/engine/base-compiler.mjs +203 -31
  18. package/engine/code-fences.mjs +4 -13
  19. package/engine/compendiums.mjs +13 -37
  20. package/engine/content-address.mjs +6 -10
  21. package/engine/content-format-check.mjs +570 -0
  22. package/engine/content-format.mjs +253 -0
  23. package/engine/content-links.mjs +144 -99
  24. package/engine/content-lint.mjs +12 -16
  25. package/engine/content-slug.mjs +2 -6
  26. package/engine/content-tables.mjs +26 -79
  27. package/engine/diagnostics.mjs +37 -15
  28. package/engine/document-subtypes.mjs +440 -0
  29. package/engine/field-reference.mjs +6 -20
  30. package/engine/field-spec.mjs +49 -45
  31. package/engine/foreign-catalog.mjs +7 -22
  32. package/engine/foreign-manifests.mjs +1 -4
  33. package/engine/frontmatter-lint.mjs +347 -43
  34. package/engine/frontmatter.mjs +3 -8
  35. package/engine/generate.mjs +36 -20
  36. package/engine/helpers.mjs +54 -81
  37. package/engine/homepage.mjs +4 -15
  38. package/engine/ids.mjs +21 -12
  39. package/engine/index.mjs +15 -0
  40. package/engine/item-registry.mjs +72 -9
  41. package/engine/journals.mjs +4 -14
  42. package/engine/kb-manifest.mjs +41 -24
  43. package/engine/macros.mjs +2 -10
  44. package/engine/manifest-emit.mjs +6 -17
  45. package/engine/map-notes.mjs +53 -87
  46. package/engine/note-claims.mjs +383 -0
  47. package/engine/note-package.mjs +1 -4
  48. package/engine/note-vocabulary.mjs +678 -0
  49. package/engine/pack-config.mjs +56 -60
  50. package/engine/pack-router.mjs +18 -8
  51. package/engine/prose-config.mjs +20 -4
  52. package/engine/prose-lint.mjs +61 -17
  53. package/engine/region-events.mjs +1 -3
  54. package/engine/retired-fields.mjs +117 -3
  55. package/engine/scene-levels.mjs +8 -22
  56. package/engine/scenes.mjs +31 -47
  57. package/engine/schema-check.mjs +348 -7
  58. package/engine/schema-extract.mjs +11 -39
  59. package/engine/site-build.mjs +13 -38
  60. package/engine/site-index.mjs +40 -35
  61. package/engine/system-block.mjs +513 -0
  62. package/engine/web-wikilinks.mjs +115 -92
  63. package/engine/wikilink-syntax.mjs +30 -0
  64. package/engine/wikilinks.mjs +147 -183
  65. package/index.mjs +1 -5
  66. package/lang.mjs +1 -3
  67. package/manifest.mjs +10 -37
  68. package/markdownlint-config.mjs +1 -5
  69. package/package.json +6 -2
  70. package/sohl/actors.mjs +251 -68
  71. package/sohl/being-info.mjs +3 -6
  72. package/sohl/document-subtypes.mjs +82 -0
  73. package/sohl/index.mjs +4 -6
  74. package/sohl/item-builders.mjs +1 -3
  75. package/sohl/item-fields.mjs +16 -34
  76. package/sohl/items.mjs +111 -17
  77. package/sohl/kb-passes.mjs +29 -39
  78. package/sohl/note-schemas.mjs +11 -7
  79. package/sohl/skill-base.mjs +7 -23
  80. package/stage.mjs +3 -13
  81. package/templates.mjs +4 -15
  82. package/types/bundle.d.mts +1 -1
  83. package/types/container.d.mts +2 -2
  84. package/types/content-config.d.mts +48 -4
  85. package/types/coverage.d.mts +1 -1
  86. package/types/e2e.d.mts +4 -4
  87. package/types/engine/address-charset.d.mts +45 -0
  88. package/types/engine/alias-index.d.mts +122 -0
  89. package/types/engine/base-compiler.d.mts +132 -4
  90. package/types/engine/content-address.d.mts +2 -2
  91. package/types/engine/content-format-check.d.mts +163 -0
  92. package/types/engine/content-format.d.mts +101 -0
  93. package/types/engine/content-links.d.mts +16 -1
  94. package/types/engine/content-lint.d.mts +6 -0
  95. package/types/engine/diagnostics.d.mts +29 -0
  96. package/types/engine/document-subtypes.d.mts +233 -0
  97. package/types/engine/field-spec.d.mts +76 -23
  98. package/types/engine/frontmatter-lint.d.mts +47 -2
  99. package/types/engine/generate.d.mts +15 -2
  100. package/types/engine/helpers.d.mts +22 -14
  101. package/types/engine/ids.d.mts +10 -0
  102. package/types/engine/index.d.mts +5 -0
  103. package/types/engine/item-registry.d.mts +21 -2
  104. package/types/engine/kb-manifest.d.mts +35 -8
  105. package/types/engine/map-notes.d.mts +21 -11
  106. package/types/engine/note-claims.d.mts +113 -0
  107. package/types/engine/note-vocabulary.d.mts +251 -0
  108. package/types/engine/pack-config.d.mts +4 -3
  109. package/types/engine/pack-router.d.mts +4 -4
  110. package/types/engine/prose-lint.d.mts +6 -2
  111. package/types/engine/retired-fields.d.mts +73 -2
  112. package/types/engine/schema-check.d.mts +182 -0
  113. package/types/engine/schema-extract.d.mts +1 -1
  114. package/types/engine/site-index.d.mts +1 -1
  115. package/types/engine/system-block.d.mts +281 -0
  116. package/types/engine/web-wikilinks.d.mts +23 -12
  117. package/types/engine/wikilink-syntax.d.mts +29 -0
  118. package/types/manifest.d.mts +1 -1
  119. package/types/sohl/actors.d.mts +62 -6
  120. package/types/sohl/document-subtypes.d.mts +14 -0
  121. package/types/sohl/index.d.mts +1 -0
  122. package/types/sohl/items.d.mts +21 -0
@@ -79,6 +79,9 @@ import { emitDiagnostic } from "./diagnostics.mjs";
79
79
  import { assertNoDeclaredPackage } from "./note-package.mjs";
80
80
  import { assertNoDraftField } from "./retired-fields.mjs";
81
81
  import { assertTypeNotRetired, packForType } from "./ids.mjs";
82
+ import { carriesSystemBlock } from "./system-block.mjs";
83
+ import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.mjs";
84
+ import { locateFrontmatterKey } from "./retired-fields.mjs";
82
85
 
83
86
  /**
84
87
  * The tallies one pass accumulates while walking the tree.
@@ -93,8 +96,9 @@ import { assertTypeNotRetired, packForType } from "./ids.mjs";
93
96
  * @property {number} compiled - Notes that became a document.
94
97
  * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
95
98
  * @property {number} skippedOther - Notes this pass does not claim.
96
- * @property {number} declined - Notes refused because they declare a retired
97
- * frontmatter field. Counted as errors, never as skips.
99
+ * @property {number} declined - Notes this pack **refused** one declaring a
100
+ * retired frontmatter field, or one routed to a system pack whose system it
101
+ * says nothing about (#58). Counted as errors, never as skips.
98
102
  */
99
103
 
100
104
  /**
@@ -167,6 +171,26 @@ export class BasePackCompiler {
167
171
  */
168
172
  static readsPackOutputOf = Object.freeze([]);
169
173
 
174
+ /**
175
+ * Whether this pass's document **is** a system's data, and therefore takes
176
+ * only notes that carry that system's block (#58).
177
+ *
178
+ * A pack may declare a `system:` — `harn-ensemble` ships an `actors-hm3`
179
+ * and an `actors-sohl` from one tree — and the note-side half of that is
180
+ * the block named after the system. A note carrying no such block has
181
+ * nothing to say about it, so compiling it there would emit a **hollow
182
+ * document**: a subtype, and none of the fields the subtype exists for.
183
+ *
184
+ * False by default, because most passes write documents that are not
185
+ * system data at all. A JournalEntry of prose is the same document under
186
+ * either system, and a journals pack that declared one must not turn every
187
+ * doc note in the tree into a finding. The Item and Actor passes say so;
188
+ * anything else that genuinely writes a system's data says so too.
189
+ *
190
+ * @type {boolean}
191
+ */
192
+ static requiresSystemBlock = false;
193
+
170
194
  /** @type {string} */
171
195
  contentBase;
172
196
  /** @type {string} */
@@ -176,6 +200,19 @@ export class BasePackCompiler {
176
200
  /** @type {number} */
177
201
  errorCount = 0;
178
202
 
203
+ /**
204
+ * Emitted-`system` findings, one per `documentType|subtype|field` (#155).
205
+ *
206
+ * A key the compiler writes is on **every** document of a subtype, so
207
+ * reporting it where it is found would print the same sentence 3,126 times
208
+ * and bury the one that is not systemic. Collected here instead and flushed
209
+ * once at the end of the pass, keyed so the class of defect is reported
210
+ * once and the first document carrying it names a file a reader can open.
211
+ *
212
+ * @type {Map<string, {message: string, file: string|undefined}>}
213
+ */
214
+ emittedFindings = new Map();
215
+
179
216
  /**
180
217
  * The pack this pass writes, and the Foundry document type it holds.
181
218
  *
@@ -244,9 +281,7 @@ export class BasePackCompiler {
244
281
  routingReporter = false,
245
282
  } = {}) {
246
283
  if (!contentBase) {
247
- throw new Error(
248
- `${this.constructor.name} compiler requires \`contentBase\``,
249
- );
284
+ throw new Error(`${this.constructor.name} compiler requires \`contentBase\``);
250
285
  }
251
286
  if (!fs.existsSync(contentBase)) {
252
287
  throw new Error(`Content tree not found at ${contentBase}`);
@@ -302,7 +337,36 @@ export class BasePackCompiler {
302
337
  */
303
338
  routesHere(fm) {
304
339
  if (!this.router || !this.packName || !this.docType) return true;
305
- return this.router.resolve(fm, this.docType) === this.packName;
340
+ return (
341
+ this.router.resolve(fm, this.docType, this.packSystem ?? undefined) === this.packName
342
+ );
343
+ }
344
+
345
+ /**
346
+ * Whether a claimed, routed note may become this pack's document at all.
347
+ *
348
+ * The pack-eligibility gate, and it fails rather than skipping: a note that
349
+ * routed *here* and carries nothing for this pack's system is an authoring
350
+ * mistake with a hollow document at the end of it, not a note that belongs
351
+ * to another pass. Skipping it quietly is how a whole tree compiles to
352
+ * documents nobody can use — the failure mode #1502 and #56 are both
353
+ * instances of.
354
+ *
355
+ * @param {object} fm - The note's frontmatter.
356
+ * @returns {boolean} True when the note may be compiled here.
357
+ * @throws {Error} When this pack's system is absent from the note. The
358
+ * error carries a `position` where the note's own file can be read.
359
+ */
360
+ eligibleFor(fm) {
361
+ if (!this.constructor.requiresSystemBlock || !this.packSystem) return true;
362
+ if (carriesSystemBlock(fm, this.packSystem)) return true;
363
+ const label = fm?.name?.full ?? fm?.shortcode ?? fm?.id ?? "this note";
364
+ throw new Error(
365
+ `${label} carries no \`${this.packSystem}:\` block, so it has no ` +
366
+ `${this.packSystem} data to compile — but it routes to pack ` +
367
+ `"${this.packName}", which declares \`system: ${this.packSystem}\`. ` +
368
+ `Add the block, or route the note to a pack of another system.`,
369
+ );
306
370
  }
307
371
 
308
372
  /**
@@ -358,10 +422,7 @@ export class BasePackCompiler {
358
422
  */
359
423
  async prepare() {
360
424
  if (this.constructor.convertsWikilinks) {
361
- this.linkIndex = buildContentLinkIndex(
362
- this.contentBase,
363
- this.router,
364
- );
425
+ this.linkIndex = buildContentLinkIndex(this.contentBase, this.router);
365
426
  this.contentDocs = collectContentDocs(this.contentBase);
366
427
  }
367
428
  this.unresolvedLinks = 0;
@@ -447,6 +508,113 @@ export class BasePackCompiler {
447
508
  });
448
509
  }
449
510
 
511
+ /**
512
+ * Report every `<system>.system` key the receiving subtype does not declare
513
+ * (#58).
514
+ *
515
+ * An **error**, not a warning: Foundry drops an unknown `system` key at
516
+ * construction without a word, so the alternative is a document shipped
517
+ * with a field the author wrote and nobody will ever see. Each finding is
518
+ * located at the offending key where the file can be read, so it points at
519
+ * a line rather than at a note.
520
+ *
521
+ * Silent where nothing can answer — no published schema, or a subtype the
522
+ * artifact does not name. `content-build lint` says that out loud once for
523
+ * the whole build rather than once per note.
524
+ *
525
+ * @param {object} fm - The note's frontmatter.
526
+ * @param {string} block - The system block to read.
527
+ * @param {string} documentType - `Item`, `Actor`, …
528
+ * @param {string} subType - The subtype this note compiles into.
529
+ * @returns {number} How many findings were reported.
530
+ */
531
+ reportUndeclaredSystemData(fm, block, documentType, subType) {
532
+ const absPath = this.currentNote?.absPath;
533
+ const findings = checkAuthoredSystemData(fm, { block, documentType, subType });
534
+ for (const finding of findings) {
535
+ this.errorCount++;
536
+ const leaf = finding.path.split(".").pop();
537
+ this.noteError(finding.message, locateFrontmatterKey(absPath, leaf));
538
+ }
539
+ return findings.length;
540
+ }
541
+
542
+ /**
543
+ * Record every `system` key the *compiled document* carries that the
544
+ * receiving subtype does not declare (#155).
545
+ *
546
+ * The sibling of {@link BasePackCompiler#reportUndeclaredSystemData}, and
547
+ * the half that sees what no declaration states. A compiler writes keys of
548
+ * its own alongside the declared fields — `shortcode`, `actionDefs`,
549
+ * `notes`, `docHtml`, `archetype` — and neither the field-declaration check
550
+ * nor the authored-`system` check can see them, so until this nothing
551
+ * compared them at all. Foundry's discard is the same silent one either
552
+ * way.
553
+ *
554
+ * Called with the block **after** the builder, the authored merge and any
555
+ * conditional fields have all written into it, so what is checked is what
556
+ * the pack file receives.
557
+ *
558
+ * Recorded rather than reported: see {@link BasePackCompiler#emittedFindings}
559
+ * for why, and {@link BasePackCompiler#reportEmittedFindings} for where they
560
+ * come out.
561
+ *
562
+ * @param {object} system - The `system` block just assembled.
563
+ * @param {object} opts
564
+ * @param {object} opts.fm - The note's frontmatter.
565
+ * @param {string} opts.block - The system block the note writes.
566
+ * @param {string} opts.documentType - `Item`, `Actor`, …
567
+ * @param {string} opts.subType - The subtype this note compiles into.
568
+ * @param {string} opts.type - The note's content type.
569
+ * @param {readonly {to?: string}[]} [opts.fields] - The type's field
570
+ * declaration, which tells a builder emission from a compiler one.
571
+ * @param {object} [opts.config] - The resolved build configuration.
572
+ * @returns {number} How many findings were new to this pass.
573
+ */
574
+ reportEmittedSystemData(system, { fm, block, documentType, subType, type, fields, config }) {
575
+ const findings = checkEmittedSystemData(system, {
576
+ fm,
577
+ block,
578
+ documentType,
579
+ subType,
580
+ type,
581
+ fields,
582
+ ...(config ? { config } : {}),
583
+ });
584
+ let added = 0;
585
+ for (const finding of findings) {
586
+ const key = `${finding.documentType}|${finding.subtype}|${finding.field}`;
587
+ if (this.emittedFindings.has(key)) continue;
588
+ this.emittedFindings.set(key, {
589
+ message: finding.message,
590
+ file: this.currentNote?.absPath,
591
+ });
592
+ added++;
593
+ }
594
+ return added;
595
+ }
596
+
597
+ /**
598
+ * Emit the collected emitted-`system` findings, once each.
599
+ *
600
+ * An **error**, for the reason #60 made its sibling one: the value is gone
601
+ * at load and the build says nothing, and severity that varied by *which
602
+ * part of the build wrote the key* would make the less fixable half the
603
+ * quieter one. What varies is the message, which says whose fix it is —
604
+ * see {@link module:engine/schema-check.emittedUndeclaredMessage}.
605
+ *
606
+ * @returns {number} How many were reported.
607
+ */
608
+ reportEmittedFindings() {
609
+ const found = this.emittedFindings;
610
+ this.emittedFindings = new Map();
611
+ for (const finding of found.values()) {
612
+ this.errorCount++;
613
+ emitDiagnostic({ file: finding.file, severity: "error", message: finding.message });
614
+ }
615
+ return found.size;
616
+ }
617
+
450
618
  /**
451
619
  * One note → one document. **Required.**
452
620
  *
@@ -457,9 +625,7 @@ export class BasePackCompiler {
457
625
  */
458
626
  // eslint-disable-next-line no-unused-vars
459
627
  buildEntry(fm, markdown) {
460
- throw new Error(
461
- `${this.constructor.name} must implement buildEntry(fm, markdown)`,
462
- );
628
+ throw new Error(`${this.constructor.name} must implement buildEntry(fm, markdown)`);
463
629
  }
464
630
 
465
631
  /**
@@ -526,9 +692,7 @@ export class BasePackCompiler {
526
692
  */
527
693
  reportCompiled(stats) {
528
694
  const label = this.constructor.label;
529
- log.info(
530
- `Compiled ${stats.compiled} ${label}${stats.compiled === 1 ? "" : "s"}`,
531
- );
695
+ log.info(`Compiled ${stats.compiled} ${label}${stats.compiled === 1 ? "" : "s"}`);
532
696
  }
533
697
 
534
698
  /**
@@ -538,9 +702,7 @@ export class BasePackCompiler {
538
702
  * @param {PassStats} stats - The pass's tallies.
539
703
  */
540
704
  reportDetail(stats) {
541
- log.debug(
542
- `Skipped ${stats.skippedOther} file(s) this pack does not claim`,
543
- );
705
+ log.debug(`Skipped ${stats.skippedOther} file(s) this pack does not claim`);
544
706
  }
545
707
 
546
708
  /**
@@ -563,8 +725,8 @@ export class BasePackCompiler {
563
725
  // them in the skipped tally is the defect (#56). Each one has
564
726
  // already been named individually as a diagnostic.
565
727
  log.error(
566
- `Declined ${stats.declined} note(s) declaring a retired ` +
567
- `frontmatter field`,
728
+ `Declined ${stats.declined} note(s) each named above, with ` +
729
+ `the reason this pack would not compile it`,
568
730
  );
569
731
  }
570
732
  this.reportDetail(stats);
@@ -588,13 +750,9 @@ export class BasePackCompiler {
588
750
  const label = this.constructor.label;
589
751
  const Label = label.charAt(0).toUpperCase() + label.slice(1);
590
752
 
591
- for (const {
592
- frontmatter: fm,
593
- body,
594
- absPath,
595
- bodyLine,
596
- bodyColumn,
597
- } of walkMarkdownTree(this.contentBase)) {
753
+ for (const { frontmatter: fm, body, absPath, bodyLine, bodyColumn } of walkMarkdownTree(
754
+ this.contentBase,
755
+ )) {
598
756
  // Which note this pass is on, so anything it calls can report a
599
757
  // position without every method having to be handed one (#17).
600
758
  this.currentNote = { absPath, bodyLine, bodyColumn };
@@ -660,14 +818,24 @@ export class BasePackCompiler {
660
818
  }
661
819
  continue;
662
820
  }
821
+ // Whether this pack's system is one the note speaks for. Checked
822
+ // after routing — a note bound for another pack is none of this
823
+ // pass's business — and before `skipNote`, so a pass's own
824
+ // rejection rules never run on a note it may not compile.
825
+ try {
826
+ this.eligibleFor(fm);
827
+ } catch (err) {
828
+ stats.declined++;
829
+ this.errorCount++;
830
+ this.noteError(err.message, err.position);
831
+ continue;
832
+ }
663
833
  if (this.skipNote(fm, body)) {
664
834
  stats.skippedOther++;
665
835
  continue;
666
836
  }
667
837
 
668
- log.debug(
669
- `Processing ${this.noteLabel(fm)}: ${resolveName(fm)} (${absPath})`,
670
- );
838
+ log.debug(`Processing ${this.noteLabel(fm)}: ${resolveName(fm)} (${absPath})`);
671
839
  try {
672
840
  const doc = this.compileNote(fm, this.convertBody(fm, body));
673
841
  stats.compiled++;
@@ -686,6 +854,10 @@ export class BasePackCompiler {
686
854
 
687
855
  this.compiledCount = stats.compiled;
688
856
  await this.finish(stats);
857
+ // After `finish`, because a pass that writes documents there (the
858
+ // scenes pass bundles an Adventure) has emitted them by now; before
859
+ // `report`, so the pass's summary counts them.
860
+ this.reportEmittedFindings();
689
861
  this.report(stats);
690
862
  }
691
863
  }
@@ -111,9 +111,7 @@ export function codeRegions(markdown, { spans = true } = {}) {
111
111
  const fence = FENCE_LINE.exec(line);
112
112
  if (fence) {
113
113
  const marker = fence[2];
114
- const closer = new RegExp(
115
- `^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`,
116
- );
114
+ const closer = new RegExp(`^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`);
117
115
  let close = i + 1;
118
116
  while (close < lines.length && !closer.test(lines[close])) close++;
119
117
  // An unclosed fence runs to the end of the document.
@@ -225,9 +223,7 @@ function inside(regions, offset) {
225
223
  */
226
224
  function offsetOf(args) {
227
225
  const last = args[args.length - 1];
228
- return typeof last === "string" ?
229
- args[args.length - 2]
230
- : args[args.length - 3];
226
+ return typeof last === "string" ? args[args.length - 2] : args[args.length - 3];
231
227
  }
232
228
 
233
229
  /**
@@ -263,9 +259,7 @@ export function replaceOutsideCode(markdown, pattern, replacer, options) {
263
259
  export function matchAllOutsideCode(markdown, pattern, options) {
264
260
  const src = String(markdown ?? "");
265
261
  const regions = codeRegions(src, options);
266
- return [...src.matchAll(pattern)].filter(
267
- (match) => !inside(regions, match.index),
268
- );
262
+ return [...src.matchAll(pattern)].filter((match) => !inside(regions, match.index));
269
263
  }
270
264
 
271
265
  /**
@@ -304,8 +298,5 @@ export function protectCode(body, transform) {
304
298
  last = region.end;
305
299
  }
306
300
  masked += src.slice(last);
307
- return transform(masked).replace(
308
- /\u0000(\d+)\u0000/g,
309
- (_m, i) => stash[Number(i)],
310
- );
301
+ return transform(masked).replace(/\u0000(\d+)\u0000/g, (_m, i) => stash[Number(i)]);
311
302
  }
@@ -87,17 +87,13 @@ export async function compilePacks({
87
87
  stageDest = config.paths.stage,
88
88
  packName,
89
89
  } = {}) {
90
- const packNames = sourcePacks.filter(
91
- (name) => !packName || name === packName,
92
- );
90
+ const packNames = sourcePacks.filter((name) => !packName || name === packName);
93
91
 
94
92
  // A prebuilt pack's JSON is checked in rather than generated, so it is
95
93
  // compiled from where it lives. Anything else comes from the build-only
96
94
  // intermediate the generation pass writes.
97
95
  const prebuiltDirs = new Map(
98
- config.packs
99
- .filter((pack) => pack.prebuilt)
100
- .map((pack) => [pack.name, pack.prebuilt]),
96
+ config.packs.filter((pack) => pack.prebuilt).map((pack) => [pack.name, pack.prebuilt]),
101
97
  );
102
98
  const needsGeneration = packNames.some((name) => !prebuiltDirs.has(name));
103
99
 
@@ -185,8 +181,7 @@ function cleanPackEntry(
185
181
  }
186
182
  delete data.flags?.importSource;
187
183
  delete data.flags?.exportSource;
188
- if (data._stats?.lastModifiedBy)
189
- data._stats.lastModifiedBy = lastModifiedBy;
184
+ if (data._stats?.lastModifiedBy) data._stats.lastModifiedBy = lastModifiedBy;
190
185
 
191
186
  // Remove empty entries in flags
192
187
  if (!data.flags) data.flags = {};
@@ -194,18 +189,11 @@ function cleanPackEntry(
194
189
  if (Object.keys(contents).length === 0) delete data.flags[key];
195
190
  });
196
191
 
197
- if (data.effects)
198
- data.effects.forEach((i) =>
199
- cleanPackEntry(i, { clearSourceId: false }),
200
- );
201
- if (data.items)
202
- data.items.forEach((i) => cleanPackEntry(i, { clearSourceId: false }));
203
- if (data.pages)
204
- data.pages.forEach((i) => cleanPackEntry(i, { ownership: -1 }));
205
- if (data.system?.description)
206
- data.system.description = cleanString(data.system.description);
207
- if (data.system?.biography)
208
- data.system.biography = cleanString(data.system.biography);
192
+ if (data.effects) data.effects.forEach((i) => cleanPackEntry(i, { clearSourceId: false }));
193
+ if (data.items) data.items.forEach((i) => cleanPackEntry(i, { clearSourceId: false }));
194
+ if (data.pages) data.pages.forEach((i) => cleanPackEntry(i, { ownership: -1 }));
195
+ if (data.system?.description) data.system.description = cleanString(data.system.description);
196
+ if (data.system?.biography) data.system.biography = cleanString(data.system.biography);
209
197
  if (data.system?.textReference)
210
198
  data.system.textReference = cleanString(data.system.textReference);
211
199
  if (data.system?.notes) data.system.notes = cleanString(data.system.notes);
@@ -249,10 +237,7 @@ export async function cleanPacks({
249
237
 
250
238
  const folders = fs
251
239
  .readdirSync(packDest, { withFileTypes: true })
252
- .filter(
253
- (file) =>
254
- file.isDirectory() && (!packName || packName === file.name),
255
- );
240
+ .filter((file) => file.isDirectory() && (!packName || packName === file.name));
256
241
 
257
242
  /**
258
243
  * Walk through directories to find JSON files.
@@ -273,9 +258,7 @@ export async function cleanPacks({
273
258
  const json = JSON.parse(await readFile(src, { encoding: "utf8" }));
274
259
  if (entryName && entryName !== json.name.toLowerCase()) continue;
275
260
  if (!json._id || !json._key) {
276
- log.info(
277
- `Failed to clean \x1b[31m${src}\x1b[0m, must have _id and _key.`,
278
- );
261
+ log.info(`Failed to clean \x1b[31m${src}\x1b[0m, must have _id and _key.`);
279
262
  continue;
280
263
  }
281
264
  cleanPackEntry(json);
@@ -351,21 +334,14 @@ export async function unpackPacks({
351
334
  await extractPack(src, dest, {
352
335
  log: true,
353
336
  transformEntry: (entry) => {
354
- if (entryName && entryName !== entry.name.toLowerCase())
355
- return false;
337
+ if (entryName && entryName !== entry.name.toLowerCase()) return false;
356
338
  cleanPackEntry(entry);
357
339
  },
358
340
  transformName: (entry) => {
359
341
  if (entry._id in folders)
360
- return path.join(
361
- "folder_",
362
- folders[entry._id].path,
363
- ".json",
364
- );
342
+ return path.join("folder_", folders[entry._id].path, ".json");
365
343
  const outputName = slugify(entry.name);
366
- const parent =
367
- containers[entry.system?.container] ??
368
- folders[entry.folder];
344
+ const parent = containers[entry.system?.container] ?? folders[entry.folder];
369
345
  return path.join(parent?.path ?? "", `${outputName}.json`);
370
346
  },
371
347
  });
@@ -38,14 +38,14 @@ export const KB_PREFIX = "kb/";
38
38
  * The URL section a note routes to.
39
39
  *
40
40
  * A `doc` is narrative content whose only identity is its subtype label, so it
41
- * routes by `category`; every other type names its own section.
41
+ * routes by `subType`; every other type names its own section.
42
42
  *
43
43
  * @param {object} fm - Parsed frontmatter.
44
44
  * @returns {string|undefined} The section, or `undefined` when the note has
45
- * none — a `doc` with no category has no address and is not published.
45
+ * none — a `doc` with no subtype has no address and is not published.
46
46
  */
47
47
  export function sectionOf(fm) {
48
- return fm.type === "doc" ? fm.category : fm.type;
48
+ return fm.type === "doc" ? fm.subType : fm.type;
49
49
  }
50
50
 
51
51
  /**
@@ -80,14 +80,12 @@ export function contentAddress(fm, name, isReadme) {
80
80
  */
81
81
  function landingOf(fm, isReadme, landing) {
82
82
  if (landing === "readme") {
83
- return isReadme ?
84
- { landing: true, segment: sectionOf(fm) }
85
- : { landing: false };
83
+ return isReadme ? { landing: true, segment: sectionOf(fm) } : { landing: false };
86
84
  }
87
85
  // `collection`. The section is authored rather than derived: it is the
88
86
  // identity of the section being introduced, and the note's own title
89
87
  // ("Creatures") is presentation, which would slug to something else.
90
- if (fm.type === "doc" && fm.category === "collection") {
88
+ if (fm.type === "doc" && fm.subType === "collection") {
91
89
  return { landing: true, segment: fm.section || fm.slug };
92
90
  }
93
91
  return { landing: false };
@@ -127,9 +125,7 @@ export function packageAddress(fm, name, { isReadme = false, scheme } = {}) {
127
125
  const land = landingOf(fm, isReadme, landing);
128
126
  if (land.landing) {
129
127
  if (typeof land.segment !== "string" || !land.segment) {
130
- throw new Error(
131
- `landing note declares no section, so it lands nowhere`,
132
- );
128
+ throw new Error(`landing note declares no section, so it lands nowhere`);
133
129
  }
134
130
  return `${prefix}${land.segment}/`;
135
131
  }