@heroiclands/package-build 19.0.0 → 20.2.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 +1100 -0
- package/CONTENT.md +264 -33
- package/README.md +43 -4
- package/bin/content-build.mjs +94 -3
- package/config.mjs +9 -1
- package/content-config.mjs +99 -19
- package/docs/content-format.md +394 -72
- package/e2e.mjs +297 -3
- package/engine/actor-compiler.mjs +197 -7
- package/engine/address-charset.mjs +23 -5
- package/engine/base-compiler.mjs +63 -2
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +92 -1
- package/engine/content-charset.mjs +434 -0
- package/engine/content-format.mjs +102 -0
- package/engine/content-icons.mjs +388 -0
- package/engine/content-index.mjs +11 -8
- package/engine/content-links.mjs +37 -21
- package/engine/field-reference.mjs +57 -5
- package/engine/field-spec.mjs +214 -7
- package/engine/folder-notes.mjs +24 -1
- package/engine/foreign-catalog.mjs +112 -7
- package/engine/foundry-entries.mjs +14 -0
- package/engine/frontmatter-lint.mjs +377 -56
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +72 -12
- package/engine/helpers.mjs +96 -10
- package/engine/index.mjs +9 -0
- package/engine/item-compiler.mjs +37 -0
- package/engine/journals.mjs +21 -4
- package/engine/macros.mjs +8 -0
- package/engine/map-notes.mjs +7 -7
- package/engine/note-claims.mjs +208 -5
- package/engine/note-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- package/engine/pack-config.mjs +102 -12
- package/engine/pack-router.mjs +0 -0
- package/engine/prose-config.mjs +42 -0
- package/engine/prose-lint.mjs +126 -0
- package/engine/retired-fields.mjs +57 -16
- package/engine/runtime-only-fields.mjs +204 -0
- package/engine/scenes.mjs +12 -19
- package/engine/schema-check.mjs +23 -1
- package/engine/schema-extract.mjs +13 -0
- package/engine/site-index.mjs +17 -0
- package/engine/subtype-registry.mjs +30 -0
- package/engine/system-block.mjs +81 -3
- package/engine/web-wikilinks.mjs +33 -27
- package/engine/wikilink-syntax.mjs +7 -0
- package/engine/wikilinks.mjs +74 -16
- package/hm3/actors.mjs +63 -13
- package/package.json +2 -2
- package/sohl/actors.mjs +106 -7
- package/sohl/item-fields.mjs +203 -0
- package/sohl/note-schemas.mjs +6 -3
- package/types/config.d.mts +7 -0
- package/types/e2e.d.mts +130 -3
- package/types/engine/actor-compiler.d.mts +83 -3
- package/types/engine/address-charset.d.mts +22 -4
- package/types/engine/base-compiler.d.mts +54 -3
- package/types/engine/content-address.d.mts +64 -0
- package/types/engine/content-charset.d.mts +127 -0
- package/types/engine/content-format.d.mts +9 -0
- package/types/engine/content-icons.d.mts +151 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +20 -0
- package/types/engine/foreign-catalog.d.mts +38 -2
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +164 -30
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +45 -9
- package/types/engine/index.d.mts +3 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-claims.d.mts +67 -0
- package/types/engine/note-ids.d.mts +14 -0
- package/types/engine/pack-config.d.mts +35 -0
- package/types/engine/prose-config.d.mts +41 -0
- package/types/engine/prose-lint.d.mts +36 -0
- package/types/engine/retired-fields.d.mts +29 -13
- package/types/engine/runtime-only-fields.d.mts +102 -0
- package/types/engine/schema-check.d.mts +10 -1
- package/types/engine/subtype-registry.d.mts +21 -0
- package/types/engine/system-block.d.mts +28 -2
- package/types/sohl/actors.d.mts +3 -3
package/engine/note-claims.mjs
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* **Which note types a configuration compiles at all** —
|
|
16
|
-
*
|
|
15
|
+
* **Which note types a configuration compiles at all** — the finding for a note
|
|
16
|
+
* whose type nothing claims (#146), and the one for a note that loses a
|
|
17
|
+
* document while the rest of it compiles (#152).
|
|
17
18
|
*
|
|
18
19
|
* Every compile pass answers one question about a note: _is this mine?_ A note
|
|
19
20
|
* every pass answers "no" to is skipped as quietly as the thousands that
|
|
@@ -50,6 +51,27 @@
|
|
|
50
51
|
* who invented a word. Collapsing the two would send `harn-ensemble` to correct
|
|
51
52
|
* five perfectly good notes.
|
|
52
53
|
*
|
|
54
|
+
* ## The partial case is a third condition, and it was invisible (#152)
|
|
55
|
+
*
|
|
56
|
+
* The table above asks whether a note is compiled *at all*, and a note that
|
|
57
|
+
* compiles one of its two documents answers yes. But a note produces more than
|
|
58
|
+
* one document as a matter of course — an item note an Item and the
|
|
59
|
+
* JournalEntry its prose becomes, a map note a Scene and a JournalEntry, an
|
|
60
|
+
* actor note an Actor and a JournalEntry since #337 — so a configuration
|
|
61
|
+
* missing a pack for *one* of them dropped that document while the rest of the
|
|
62
|
+
* note compiled into a pack that does exist. The build succeeded and shipped
|
|
63
|
+
* half of what was written.
|
|
64
|
+
*
|
|
65
|
+
* | condition | what it means | whose fix |
|
|
66
|
+
* | --- | --- | --- |
|
|
67
|
+
* | some documents have a pack, one does not | this note compiles, and one of its documents is lost | configuration |
|
|
68
|
+
*
|
|
69
|
+
* {@link documentClassesFor} is the question this needs and the type-level
|
|
70
|
+
* table could not answer: not "is anything claiming this note" but "which
|
|
71
|
+
* documents does this note produce", asked per note, because documentation is
|
|
72
|
+
* per note — a doc-carrying note with an empty body produces no JournalEntry at
|
|
73
|
+
* all, and `Journals.skipNote` is where that is decided.
|
|
74
|
+
*
|
|
53
75
|
* ## The claim table mirrors `selects`, and a test holds them together
|
|
54
76
|
*
|
|
55
77
|
* Which note types a pass claims is stated by that pass's `selects`, and the
|
|
@@ -61,7 +83,7 @@
|
|
|
61
83
|
* @module
|
|
62
84
|
*/
|
|
63
85
|
|
|
64
|
-
import { assertSuppliedCorpus } from "./helpers.mjs";
|
|
86
|
+
import { assertSuppliedCorpus, parseMarkdownFile } from "./helpers.mjs";
|
|
65
87
|
// The record accessors only: this module is imported by the content index, so
|
|
66
88
|
// importing the index back would close a cycle (#243).
|
|
67
89
|
import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
|
|
@@ -252,6 +274,115 @@ export function noteTypesClaimedBy(docType, sources) {
|
|
|
252
274
|
}
|
|
253
275
|
}
|
|
254
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Every Foundry document class {@link noteTypesClaimedBy} answers for.
|
|
279
|
+
*
|
|
280
|
+
* The switch above, read the other way round. It is written out rather than
|
|
281
|
+
* derived because a `switch` cannot be enumerated — and
|
|
282
|
+
* `tests/unclaimed-note-types.test.ts` holds the two together by checking that
|
|
283
|
+
* no class outside this list claims anything, so a row added there and not here
|
|
284
|
+
* fails rather than going quiet.
|
|
285
|
+
*
|
|
286
|
+
* Order is the order a reader meets them in a message, not a precedence.
|
|
287
|
+
*
|
|
288
|
+
* @type {readonly string[]}
|
|
289
|
+
*/
|
|
290
|
+
export const CLAIMABLE_DOCUMENT_TYPES = Object.freeze([
|
|
291
|
+
"Item",
|
|
292
|
+
"Actor",
|
|
293
|
+
"JournalEntry",
|
|
294
|
+
"Macro",
|
|
295
|
+
"Scene",
|
|
296
|
+
"Adventure",
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Every document class a note of one type compiles into (#152).
|
|
301
|
+
*
|
|
302
|
+
* **A note produces more than one document, and that is the ordinary case.** An
|
|
303
|
+
* item note compiles an Item *and* the JournalEntry its prose becomes; a map
|
|
304
|
+
* note a Scene and a JournalEntry; since #337 an actor note an Actor and a
|
|
305
|
+
* JournalEntry too. {@link claimedNoteTypes} unions over the configured packs
|
|
306
|
+
* and so answers "is this note compiled *at all*", which is #146's question and
|
|
307
|
+
* cannot see a note that compiles one of its two documents and loses the other.
|
|
308
|
+
*
|
|
309
|
+
* Asked of the **claim table** rather than of a list of its own, so the set of
|
|
310
|
+
* documents a type produces and the set of passes that claim it are one
|
|
311
|
+
* statement. A pass that starts claiming a type starts producing its document
|
|
312
|
+
* here, with nothing to remember.
|
|
313
|
+
*
|
|
314
|
+
* **Union across systems, never per system.** A type one system maps and
|
|
315
|
+
* another does not appears once, because the `Item` and `Actor` rows already
|
|
316
|
+
* fold the maps together — so this cannot report a document class a system
|
|
317
|
+
* deliberately declines to produce, which is the silence #79 requires.
|
|
318
|
+
*
|
|
319
|
+
* ## The JournalEntry row is the one that is per *note*
|
|
320
|
+
*
|
|
321
|
+
* Every other row is a property of the type: a `macro` note produces a Macro, a
|
|
322
|
+
* map note a Scene, whatever either says. Documentation is not. `Journals`
|
|
323
|
+
* declines a doc-carrying note whose body is empty — *"an item with no prose
|
|
324
|
+
* gets no doc, and the items pass leaves its description empty rather than
|
|
325
|
+
* pointing at nothing"* — so whether an item note produces a JournalEntry is
|
|
326
|
+
* decided by the note, not by its type.
|
|
327
|
+
*
|
|
328
|
+
* That distinction is the whole difference between a useful finding and a
|
|
329
|
+
* useless one. `sohl-kethira-basic` declares no JournalEntry pack and ships 393
|
|
330
|
+
* notes whose descriptions are *deliberately* empty, under the Fan Material
|
|
331
|
+
* Guidelines its configuration explains at length. A type-level answer would
|
|
332
|
+
* report every one of them for losing a document none of them produces. Asking
|
|
333
|
+
* per note, it reports none, and still reports `harn-ensemble`'s 2,517 beings,
|
|
334
|
+
* whose `{#appearance}` and `{#dossier}` prose is real and is lost.
|
|
335
|
+
*
|
|
336
|
+
* `hasProse` is therefore how the caller answers that, and it is a **thunk** so
|
|
337
|
+
* that the file is read only where the answer could change the outcome. Omitted,
|
|
338
|
+
* the answer is the type's full potential — every document such a note *could*
|
|
339
|
+
* produce — which is what a caller asking about a type rather than a note wants.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} type - The note's declared `type`, current spelling.
|
|
342
|
+
* @param {ClaimSources} [sources] - What to answer from.
|
|
343
|
+
* @param {object} [opts] - Options.
|
|
344
|
+
* @param {(() => boolean)|boolean} [opts.hasProse] - Whether *this note* carries
|
|
345
|
+
* a body. Omitted, the type's potential is reported.
|
|
346
|
+
* @returns {string[]} The document classes, in {@link CLAIMABLE_DOCUMENT_TYPES}
|
|
347
|
+
* order. Empty for a type nothing compiles.
|
|
348
|
+
*/
|
|
349
|
+
export function documentClassesFor(type, sources, { hasProse } = {}) {
|
|
350
|
+
const resolved = resolveSources(sources);
|
|
351
|
+
return CLAIMABLE_DOCUMENT_TYPES.filter((docType) => {
|
|
352
|
+
if (!noteTypesClaimedBy(docType, resolved).has(type)) return false;
|
|
353
|
+
if (docType !== "JournalEntry") return true;
|
|
354
|
+
// A type whose *whole* document is the journal always produces one;
|
|
355
|
+
// there is no body condition, because the body is the document.
|
|
356
|
+
if (JOURNAL_TYPES.has(type)) return true;
|
|
357
|
+
if (hasProse === undefined) return true;
|
|
358
|
+
return Boolean(typeof hasProse === "function" ? hasProse() : hasProse);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Whether a note carries a body at all — the condition `Journals.skipNote`
|
|
364
|
+
* applies, asked from the outside (#152).
|
|
365
|
+
*
|
|
366
|
+
* Read from the file rather than from the index record, because a record
|
|
367
|
+
* carries a note's frontmatter and its derived address and not its prose. The
|
|
368
|
+
* walk that calls this is already reading the same file to locate the `type:`
|
|
369
|
+
* key for a finding's position, so this is the same cost in the same place —
|
|
370
|
+
* and it is called only for a note whose documentation would otherwise be
|
|
371
|
+
* reported as lost.
|
|
372
|
+
*
|
|
373
|
+
* @param {string} absPath - The note's path.
|
|
374
|
+
* @returns {boolean} True when the body has content.
|
|
375
|
+
*/
|
|
376
|
+
function noteHasProse(absPath) {
|
|
377
|
+
try {
|
|
378
|
+
return Boolean(parseMarkdownFile(absPath).body);
|
|
379
|
+
} catch {
|
|
380
|
+
// Unreadable here means unreadable for the compile too, which reports
|
|
381
|
+
// it with a message about the file rather than about its documentation.
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
255
386
|
/**
|
|
256
387
|
* Every note type some pack in a configuration would compile.
|
|
257
388
|
*
|
|
@@ -418,6 +549,44 @@ function specifiedMessage(type) {
|
|
|
418
549
|
);
|
|
419
550
|
}
|
|
420
551
|
|
|
552
|
+
/**
|
|
553
|
+
* The **partial** finding: the note compiles, and one of its documents does not
|
|
554
|
+
* (#152).
|
|
555
|
+
*
|
|
556
|
+
* #146's question is "does anything claim this note", and the answer is yes —
|
|
557
|
+
* which is exactly why this went unreported. A note produces more than one
|
|
558
|
+
* document, and a configuration missing a pack for one of them drops that
|
|
559
|
+
* document while the rest of the note compiles into a pack that does exist. The
|
|
560
|
+
* build succeeds, the compendium ships, and the missing half is discoverable
|
|
561
|
+
* only by noticing it is not there.
|
|
562
|
+
*
|
|
563
|
+
* The message names the note, the document class with no pack, and the class
|
|
564
|
+
* that *did* compile — the last because it is what distinguishes this from
|
|
565
|
+
* #146's finding at a glance: the note is not unclaimed, it is half-claimed, and
|
|
566
|
+
* the fix is a pack rather than a `type:`.
|
|
567
|
+
*
|
|
568
|
+
* @param {string} type - The note's declared `type`.
|
|
569
|
+
* @param {readonly string[]} missing - Document classes with no pack.
|
|
570
|
+
* @param {readonly string[]} compiled - Document classes that do have one.
|
|
571
|
+
* @returns {string} The message.
|
|
572
|
+
*/
|
|
573
|
+
function partialMessage(type, missing, compiled) {
|
|
574
|
+
// `Item`, `Actor` and `Adventure` take "an". Spelled out rather than left
|
|
575
|
+
// to read as a typo in a message an author meets at the moment they are
|
|
576
|
+
// being told something went wrong.
|
|
577
|
+
const article = (name) => (/^[AEIOU]/.test(name) ? "an" : "a");
|
|
578
|
+
const list = (classes) => classes.map((name) => `${article(name)} ${name}`).join(" and ");
|
|
579
|
+
const names = (classes) => classes.join(" and ");
|
|
580
|
+
return (
|
|
581
|
+
`a note of type "${type}" compiles into ${list(missing)} as well as ` +
|
|
582
|
+
`${list(compiled)}, and \`packs:\` declares no ${names(missing)} pack — ` +
|
|
583
|
+
`so the ${names(missing)} is dropped with no error while the rest of the ` +
|
|
584
|
+
`note compiles. Declare ${list(missing)} pack in ` +
|
|
585
|
+
`package-build.config.yaml, or accept the loss deliberately by not ` +
|
|
586
|
+
`authoring what it would have carried.`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
421
590
|
/**
|
|
422
591
|
* The **authoring** finding: nothing anywhere knows the type.
|
|
423
592
|
*
|
|
@@ -455,9 +624,13 @@ function authoringMessage(type) {
|
|
|
455
624
|
*/
|
|
456
625
|
export function unclaimedNoteFindings(config = loadPackConfig(), sources, { records } = {}) {
|
|
457
626
|
const resolved = resolveSources(sources);
|
|
458
|
-
const claimed = claimedNoteTypes(config, resolved);
|
|
459
627
|
const vocabulary = noteTypeVocabulary(resolved);
|
|
460
628
|
const findings = [];
|
|
629
|
+
// The document classes this configuration can actually receive a compiled
|
|
630
|
+
// document into. A **prebuilt** pack is not one of them, for the reason
|
|
631
|
+
// {@link claimedNoteTypes} states: its JSON is checked in, it has no pass,
|
|
632
|
+
// and no note is routed into it.
|
|
633
|
+
const configured = new Set((config.packs ?? []).filter((p) => !p.prebuilt).map((p) => p.type));
|
|
461
634
|
|
|
462
635
|
// The corpus this compile derived once (#243), required rather than
|
|
463
636
|
// derived here: this module is imported *by* the content index, so it
|
|
@@ -481,7 +654,37 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
|
|
|
481
654
|
// authored one (#78). The rename itself is reported by the frontmatter
|
|
482
655
|
// lint, which can say what to write instead.
|
|
483
656
|
const current = currentType(type);
|
|
484
|
-
|
|
657
|
+
|
|
658
|
+
// Every document this note produces, against the classes this
|
|
659
|
+
// configuration has a pack for. Three outcomes, and the middle one is
|
|
660
|
+
// #152's — it was invisible while the question was only "is anything
|
|
661
|
+
// claiming this note", because the answer there is yes.
|
|
662
|
+
const produces = documentClassesFor(current, resolved, {
|
|
663
|
+
// Lazy: only a doc-carrying type whose JournalEntry has nowhere to
|
|
664
|
+
// go asks, so a tree with a JournalEntry pack — which is most of
|
|
665
|
+
// them — reads no bodies at all.
|
|
666
|
+
hasProse: () => noteHasProse(absPath),
|
|
667
|
+
});
|
|
668
|
+
const missing = produces.filter((docType) => !configured.has(docType));
|
|
669
|
+
const compiled = produces.filter((docType) => configured.has(docType));
|
|
670
|
+
|
|
671
|
+
// Every document it produces has somewhere to go.
|
|
672
|
+
if (produces.length && !missing.length) continue;
|
|
673
|
+
|
|
674
|
+
// Some do and some do not: the note compiles, and one of its documents
|
|
675
|
+
// is dropped in silence. A type nothing produces at all falls past this
|
|
676
|
+
// to the #146 messages below, where `produces` being empty is itself
|
|
677
|
+
// part of the answer.
|
|
678
|
+
if (compiled.length) {
|
|
679
|
+
findings.push({
|
|
680
|
+
file: absPath,
|
|
681
|
+
...locateFrontmatterKey(absPath, "type", type),
|
|
682
|
+
severity: /** @type {"error"} */ ("error"),
|
|
683
|
+
type,
|
|
684
|
+
message: partialMessage(type, missing, compiled),
|
|
685
|
+
});
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
485
688
|
|
|
486
689
|
findings.push({
|
|
487
690
|
file: absPath,
|
package/engine/note-ids.mjs
CHANGED
|
@@ -44,6 +44,11 @@ import { documentId } from "./content-address.mjs";
|
|
|
44
44
|
import { systemOf } from "./document-subtypes.mjs";
|
|
45
45
|
import { contentPackage } from "./content-package.mjs";
|
|
46
46
|
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
47
|
+
// The folder id's derivation, taken from the pass that owns it rather than
|
|
48
|
+
// restated here — see the `folder` branch below. `folder-notes.mjs` reaches
|
|
49
|
+
// only `content-address`, `address-charset`, `ids` and `retired-fields`, none
|
|
50
|
+
// of which reach this module, so the direction closes no cycle.
|
|
51
|
+
import { FOLDER_TYPE, folderDocId } from "./folder-notes.mjs";
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
54
|
* A frontmatter value read as a non-blank string, or `undefined`.
|
|
@@ -64,6 +69,20 @@ function text(value) {
|
|
|
64
69
|
/**
|
|
65
70
|
* The document id a note compiles under: its pin, or its address.
|
|
66
71
|
*
|
|
72
|
+
* **One type hashes its address differently, and that is not an exception to
|
|
73
|
+
* the rule but an application of it.** A `Folder` is a document of its own
|
|
74
|
+
* class, and its id is hashed under the `folder` namespace so that a folder and
|
|
75
|
+
* an item sharing a shortcode cannot derive one id — a collision Foundry would
|
|
76
|
+
* not report, since it keys folders and documents in separate collections
|
|
77
|
+
* (#258). So the answer for a folder comes from
|
|
78
|
+
* {@link module:engine/folder-notes.folderDocId}, the pass that emits those
|
|
79
|
+
* documents, rather than from a second derivation here.
|
|
80
|
+
*
|
|
81
|
+
* That this function ever answered differently was invisible from inside a
|
|
82
|
+
* build — no pass reads a folder's id from here — and surfaced only in the
|
|
83
|
+
* content index, which is read from outside and had no way to be checked
|
|
84
|
+
* against what shipped (#310).
|
|
85
|
+
*
|
|
67
86
|
* Returns `undefined` for a file with **no address** — no `type`, or no
|
|
68
87
|
* `shortcode`. Such a file is not an addressable note, so it has no document
|
|
69
88
|
* and inventing an id for one would file it under nothing. Every caller already
|
|
@@ -88,7 +107,12 @@ export function noteDocId(fm, { pkg, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS } = {})
|
|
|
88
107
|
const type = text(fm.type);
|
|
89
108
|
const shortcode = text(fm.shortcode);
|
|
90
109
|
if (!type || !shortcode) return undefined;
|
|
91
|
-
|
|
110
|
+
const owner = pkg ?? contentPackage();
|
|
111
|
+
// Lowercased because `collectFolderNotes` matches the type that way, and
|
|
112
|
+
// the two must answer alike about the same note or the divergence this
|
|
113
|
+
// branch closes reopens under a capitalised `type: Folder`.
|
|
114
|
+
if (type.toLowerCase() === FOLDER_TYPE) return folderDocId(owner, shortcode);
|
|
115
|
+
return documentId(owner, systemOf(type, maps), type, shortcode);
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
/**
|
|
@@ -422,7 +422,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
422
422
|
// values here would put a second, weaker answer beside the real one.
|
|
423
423
|
subTypes: null,
|
|
424
424
|
data: Object.freeze([
|
|
425
|
-
{
|
|
425
|
+
{
|
|
426
|
+
name: "portrait",
|
|
427
|
+
...TEXT,
|
|
428
|
+
describe:
|
|
429
|
+
"Path to the portrait image. Its first segment says which package owns " +
|
|
430
|
+
"the file: `systems/…` and `modules/…` are emitted unchanged, anything " +
|
|
431
|
+
"else is this package's own and is rooted under its assets.",
|
|
432
|
+
},
|
|
426
433
|
TEMPLATE_PRIORITY,
|
|
427
434
|
{ name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
|
|
428
435
|
{ name: "occupation", ...TEXT, describe: "What the being does for a living." },
|
|
@@ -466,7 +473,14 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
466
473
|
vehicle: Object.freeze({
|
|
467
474
|
subTypes: null,
|
|
468
475
|
data: Object.freeze([
|
|
469
|
-
{
|
|
476
|
+
{
|
|
477
|
+
name: "portrait",
|
|
478
|
+
...TEXT,
|
|
479
|
+
describe:
|
|
480
|
+
"Path to the portrait image. Its first segment says which package owns " +
|
|
481
|
+
"the file: `systems/…` and `modules/…` are emitted unchanged, anything " +
|
|
482
|
+
"else is this package's own and is rooted under its assets.",
|
|
483
|
+
},
|
|
470
484
|
TEMPLATE_PRIORITY,
|
|
471
485
|
]),
|
|
472
486
|
}),
|
|
@@ -577,16 +591,31 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
577
591
|
...TEXT,
|
|
578
592
|
describe: "Roll formula for the delay between contraction and onset.",
|
|
579
593
|
},
|
|
594
|
+
{
|
|
595
|
+
name: "onsetDurationBase",
|
|
596
|
+
...NUM,
|
|
597
|
+
describe: "That delay in seconds, stated outright instead of rolled.",
|
|
598
|
+
},
|
|
580
599
|
{
|
|
581
600
|
name: "healingCheckDurationFormula",
|
|
582
601
|
...TEXT,
|
|
583
602
|
describe: "Roll formula for the interval between healing checks.",
|
|
584
603
|
},
|
|
604
|
+
{
|
|
605
|
+
name: "healingCheckDurationBase",
|
|
606
|
+
...NUM,
|
|
607
|
+
describe: "That interval in seconds, stated outright instead of rolled.",
|
|
608
|
+
},
|
|
585
609
|
{
|
|
586
610
|
name: "resolutionDurationFormula",
|
|
587
611
|
...TEXT,
|
|
588
612
|
describe: "Roll formula for the time from onset to resolution.",
|
|
589
613
|
},
|
|
614
|
+
{
|
|
615
|
+
name: "resolutionDurationBase",
|
|
616
|
+
...NUM,
|
|
617
|
+
describe: "That time in seconds, stated outright instead of rolled.",
|
|
618
|
+
},
|
|
590
619
|
]),
|
|
591
620
|
}),
|
|
592
621
|
|
|
@@ -719,7 +748,39 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
719
748
|
"shock",
|
|
720
749
|
"coma",
|
|
721
750
|
]),
|
|
722
|
-
data: Object.freeze([
|
|
751
|
+
data: Object.freeze([
|
|
752
|
+
TEMPLATE_PRIORITY,
|
|
753
|
+
{
|
|
754
|
+
name: "healingCheckDurationFormula",
|
|
755
|
+
...TEXT,
|
|
756
|
+
describe: "Roll formula for the interval between healing checks.",
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "healingCheckDurationBase",
|
|
760
|
+
...NUM,
|
|
761
|
+
describe: "That interval in seconds, stated outright instead of rolled.",
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
name: "bloodLossAdvanceDurationFormula",
|
|
765
|
+
...TEXT,
|
|
766
|
+
describe: "Roll formula for the interval between blood-loss advances.",
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "bloodLossAdvanceDurationBase",
|
|
770
|
+
...NUM,
|
|
771
|
+
describe: "That interval in seconds. Setting it is what makes the wound bleed.",
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: "courseDurationFormula",
|
|
775
|
+
...TEXT,
|
|
776
|
+
describe: "Roll formula for the interval between course tests.",
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: "courseDurationBase",
|
|
780
|
+
...NUM,
|
|
781
|
+
describe: "That interval in seconds, stated outright instead of rolled.",
|
|
782
|
+
},
|
|
783
|
+
]),
|
|
723
784
|
}),
|
|
724
785
|
|
|
725
786
|
weapongear: Object.freeze({
|
|
@@ -809,6 +870,7 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
809
870
|
"folk",
|
|
810
871
|
"culture",
|
|
811
872
|
"bestiary",
|
|
873
|
+
"gathering",
|
|
812
874
|
]),
|
|
813
875
|
// Nothing of its own: a lore note is prose, and what it *is* about is
|
|
814
876
|
// its subType. The specification declares an empty table for it, and
|
|
@@ -881,12 +943,17 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
881
943
|
// derived for them, which is precisely what a subType decides (#174).
|
|
882
944
|
subTypes: Object.freeze(["battlemap", "localmap", "regionalmap"]),
|
|
883
945
|
data: Object.freeze([
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
946
|
+
// `img`, as every other note type spells its artwork. A map alone
|
|
947
|
+
// read `image` out of its `sohl:` block; that spelling is retired
|
|
948
|
+
// and gone (#149), so the two names are one again.
|
|
949
|
+
{
|
|
950
|
+
name: "img",
|
|
951
|
+
...TEXT,
|
|
952
|
+
describe:
|
|
953
|
+
"Path to the map art, owned by whichever package its first segment " +
|
|
954
|
+
"names — `systems/…` and `modules/…` unchanged, anything else this " +
|
|
955
|
+
"package's own.",
|
|
956
|
+
},
|
|
890
957
|
{
|
|
891
958
|
name: "dimensions",
|
|
892
959
|
...LIST,
|
package/engine/pack-config.mjs
CHANGED
|
@@ -47,14 +47,38 @@
|
|
|
47
47
|
* actually needed: every accessor in the engine funnels through here, so
|
|
48
48
|
* anything that reads configuration throws with the message below.
|
|
49
49
|
*
|
|
50
|
-
* **Located by walking up from
|
|
51
|
-
* The config file sits at the root of the repository
|
|
52
|
-
*
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
50
|
+
* **Located by walking up from the working directory, and from this module only
|
|
51
|
+
* when that finds nothing.** The config file sits at the root of the repository
|
|
52
|
+
* being built, and a build is run inside that repository — so the walk from
|
|
53
|
+
* `process.cwd()` finds it from the root, from `packages/`, from anywhere
|
|
54
|
+
* below. Climbing from this module instead finds the same file too, right up
|
|
55
|
+
* until the installed package is not the one the caller is standing in: a git
|
|
56
|
+
* worktree nested under its parent checkout with no `node_modules` of its own
|
|
57
|
+
* resolves `@heroiclands/package-build` out of the *parent's*, because Node's
|
|
58
|
+
* resolution walks parent directories. `import.meta.dirname` is then inside the
|
|
59
|
+
* parent, the walk lands on the parent's configuration, and the build compiles
|
|
60
|
+
* the parent's content tree into the parent's `build/` and exits 0 — saying so
|
|
61
|
+
* only in paths that are easy to read past (#364).
|
|
62
|
+
*
|
|
63
|
+
* That failure is undetectable on exactly the work most likely to provoke it.
|
|
64
|
+
* The usual tell is a zero diff where a change was expected; an
|
|
65
|
+
* output-preserving sweep *expects* zero differences, so the tell is gone and a
|
|
66
|
+
* wrong-tree build produces confident evidence for a tree nobody touched. Order
|
|
67
|
+
* of resolution is the fix, because reviewer vigilance cannot be.
|
|
68
|
+
*
|
|
69
|
+
* The module walk stays, as the fallback for an invocation from outside any
|
|
70
|
+
* repository, and is never *preferred*: a configuration found above the
|
|
71
|
+
* installed package rather than above the working directory is not the one a
|
|
72
|
+
* caller meant. When both walks find one and they disagree, the ignored one is
|
|
73
|
+
* named in a warning rather than passed over — that disagreement is also the
|
|
74
|
+
* only cheap signal that this tree is building on another checkout's
|
|
75
|
+
* `node_modules`. `PACKAGE_BUILD_CONFIG` names the file explicitly and skips
|
|
76
|
+
* both walks, which is why it was the workaround.
|
|
77
|
+
*
|
|
78
|
+
* What #1508 removed stays removed. The property it bought was not "resolve
|
|
79
|
+
* from the module"; it was that a build reads one tree however it was launched,
|
|
80
|
+
* and an upward walk from the working directory keeps that — every directory
|
|
81
|
+
* inside a repository resolves that repository's single configuration.
|
|
58
82
|
*
|
|
59
83
|
* **Loaded synchronously.** A YAML config is parsed synchronously as a matter
|
|
60
84
|
* of course; an `.mjs` one is loaded with `require` rather than `await import`,
|
|
@@ -81,7 +105,12 @@ import { createRequire } from "node:module";
|
|
|
81
105
|
import YAML from "yaml";
|
|
82
106
|
|
|
83
107
|
import { defineConfig, DERIVED_SYSTEM_VERSION } from "../content-config.mjs";
|
|
84
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
emitDiagnostic,
|
|
110
|
+
formatDiagnostic,
|
|
111
|
+
positionOfYamlPath,
|
|
112
|
+
yamlKeyPath,
|
|
113
|
+
} from "./diagnostics.mjs";
|
|
85
114
|
|
|
86
115
|
/** The stem every consuming repository declares its build under. */
|
|
87
116
|
export const CONFIG_BASENAME = "package-build.config";
|
|
@@ -144,6 +173,41 @@ export function findConfigFile(from) {
|
|
|
144
173
|
}
|
|
145
174
|
}
|
|
146
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Which configuration file a build launched here should read, and what each
|
|
178
|
+
* walk found.
|
|
179
|
+
*
|
|
180
|
+
* Kept separate from {@link loadPackConfig} because the *choice* is worth being
|
|
181
|
+
* able to ask about without loading anything: the two walks disagreeing is the
|
|
182
|
+
* observable form of #364, and a caller that wants to report it — or a test
|
|
183
|
+
* that wants to describe it — should not have to reproduce the resolution and
|
|
184
|
+
* risk disagreeing with the loader about it. It performs I/O, and is named for
|
|
185
|
+
* it, like the {@link findConfigFile} it calls twice.
|
|
186
|
+
*
|
|
187
|
+
* `PACKAGE_BUILD_CONFIG` is deliberately not consulted here. An explicit name
|
|
188
|
+
* is not a search result: {@link loadPackConfig} short-circuits on it before it
|
|
189
|
+
* ever asks, so there is no walk to report and nothing to disagree with.
|
|
190
|
+
*
|
|
191
|
+
* @param {object} [from] - Where to walk up from; both default to the real
|
|
192
|
+
* thing, and are parameters only so a caller can describe a tree it is not
|
|
193
|
+
* standing in.
|
|
194
|
+
* @param {string} [from.cwd] - The directory the build was launched in.
|
|
195
|
+
* @param {string} [from.moduleDir] - The directory this module sits in.
|
|
196
|
+
* @returns {{path: string|undefined, fromCwd: string|undefined, fromModule: string|undefined}}
|
|
197
|
+
* The file to read, and each walk's own answer — the same file in an ordinary
|
|
198
|
+
* build, different ones in a worktree resolving the toolchain out of its
|
|
199
|
+
* parent checkout.
|
|
200
|
+
* @throws {Error} As {@link findConfigFile}, when one directory holds more than
|
|
201
|
+
* one configuration.
|
|
202
|
+
*/
|
|
203
|
+
export function resolveConfigFile({ cwd = process.cwd(), moduleDir = import.meta.dirname } = {}) {
|
|
204
|
+
const fromCwd = findConfigFile(cwd);
|
|
205
|
+
const fromModule = findConfigFile(moduleDir);
|
|
206
|
+
// `??`, not `||`: the module walk is a fallback for finding *nothing*, never
|
|
207
|
+
// a tie-break between two answers.
|
|
208
|
+
return { path: fromCwd ?? fromModule, fromCwd, fromModule };
|
|
209
|
+
}
|
|
210
|
+
|
|
147
211
|
const require = createRequire(import.meta.url);
|
|
148
212
|
|
|
149
213
|
/**
|
|
@@ -607,20 +671,46 @@ export function loadPackConfig() {
|
|
|
607
671
|
if (loaded) return loaded;
|
|
608
672
|
|
|
609
673
|
const explicit = process.env.PACKAGE_BUILD_CONFIG;
|
|
610
|
-
const
|
|
674
|
+
const found = explicit ? undefined : resolveConfigFile();
|
|
675
|
+
const configPath = explicit ? path.resolve(explicit) : found.path;
|
|
611
676
|
|
|
612
677
|
if (!configPath || !fs.existsSync(configPath)) {
|
|
613
678
|
throw new Error(
|
|
614
679
|
explicit ?
|
|
615
680
|
`package-build: PACKAGE_BUILD_CONFIG names ${configPath}, ` +
|
|
616
681
|
`which does not exist.`
|
|
682
|
+
// Both origins, because either walk could have found one and
|
|
683
|
+
// naming only the module's would send a reader looking inside
|
|
684
|
+
// `node_modules/` for a file that belongs in their own root.
|
|
617
685
|
: `package-build: no ${CONFIG_FILENAMES.join(" or ")} found at ` +
|
|
618
|
-
`or above ${
|
|
619
|
-
|
|
686
|
+
`or above ${process.cwd()}, nor at or above ` +
|
|
687
|
+
`${import.meta.dirname}. A consuming repository declares ` +
|
|
688
|
+
`its build in one file at its root; set ` +
|
|
620
689
|
`PACKAGE_BUILD_CONFIG to name it elsewhere.`,
|
|
621
690
|
);
|
|
622
691
|
}
|
|
623
692
|
|
|
693
|
+
// Two different files, one of which is about to be ignored. Said out loud
|
|
694
|
+
// because the alternative is what #364 was: a build that reads the parent
|
|
695
|
+
// checkout's configuration, compiles the parent's tree, and reports it only
|
|
696
|
+
// in absolute paths nobody rereads. A warning rather than an error — the
|
|
697
|
+
// shape is legitimate, and the working directory's answer is the right one
|
|
698
|
+
// — but never silence.
|
|
699
|
+
if (found?.fromCwd && found.fromModule && found.fromCwd !== found.fromModule) {
|
|
700
|
+
emitDiagnostic({
|
|
701
|
+
severity: "warning",
|
|
702
|
+
message:
|
|
703
|
+
`package-build: reading ${found.fromCwd}, the configuration ` +
|
|
704
|
+
`above this working directory. The installed ` +
|
|
705
|
+
`@heroiclands/package-build sits under a different ` +
|
|
706
|
+
`repository, whose own ${found.fromModule} is being ignored — ` +
|
|
707
|
+
`usually because this tree has no \`node_modules\` of its own ` +
|
|
708
|
+
`and resolved the toolchain out of a parent checkout. Run ` +
|
|
709
|
+
`\`npm ci\` here, or set PACKAGE_BUILD_CONFIG, to say which ` +
|
|
710
|
+
`tree is meant.`,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
624
714
|
loaded =
|
|
625
715
|
configPath.endsWith(".mjs") ?
|
|
626
716
|
loadCodeConfig(configPath)
|
package/engine/pack-router.mjs
CHANGED
|
Binary file
|
package/engine/prose-config.mjs
CHANGED
|
@@ -134,6 +134,48 @@ export function sharedPrettierOptionsFor(file) {
|
|
|
134
134
|
return /\.md$/i.test(file) ? { ...PRETTIER_BASE, ...PRETTIER_MARKDOWN } : { ...PRETTIER_BASE };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Where a resolved Prettier configuration disagrees with the shared one.
|
|
139
|
+
*
|
|
140
|
+
* The runner resolves each file's options as *either* the consumer's own config
|
|
141
|
+
* or {@link sharedPrettierOptionsFor}, never a merge. That is what bare Prettier
|
|
142
|
+
* does and it is the contract — but it means the conventions this package exists
|
|
143
|
+
* to publish hold by convention alone, and they lapse in two opposite directions
|
|
144
|
+
* (#133). A consumer that declares any config of its own gets whatever that
|
|
145
|
+
* config says: spread {@link PRETTIER_BASE} without the markdown override and
|
|
146
|
+
* every note reindents at 4, the reindentation the override was added to prevent
|
|
147
|
+
* (#76); write a partial `.prettierrc` such as `{"tabWidth": 2}` and
|
|
148
|
+
* `printWidth`, `trailingComma`, `experimentalTernaries` and the rest fall back
|
|
149
|
+
* to Prettier's own defaults. A consumer that declares *nothing* formats one way
|
|
150
|
+
* under this command and another under a bare `npx prettier`.
|
|
151
|
+
*
|
|
152
|
+
* This is the comparison that makes either absence visible. It is a **report,
|
|
153
|
+
* not a merge**: what a consumer declared still wins, and a deliberate local
|
|
154
|
+
* choice stays possible — it stops being silent, and nothing here fails a build
|
|
155
|
+
* over it.
|
|
156
|
+
*
|
|
157
|
+
* Every shared value is a primitive, so `!==` is the whole comparison. An option
|
|
158
|
+
* holding an object would need a deeper one, and the shared set has none —
|
|
159
|
+
* `overrides` is not compared, because `resolveConfig` has already applied and
|
|
160
|
+
* removed it by the time a configuration reaches this.
|
|
161
|
+
*
|
|
162
|
+
* @param {object|null|undefined} resolved - What `prettier.resolveConfig`
|
|
163
|
+
* returned for `file`, with the consumer's own `overrides` already applied.
|
|
164
|
+
* `null` — no configuration at all — reports every shared key as absent.
|
|
165
|
+
* @param {string} file - Path the options were resolved for. Decides whether
|
|
166
|
+
* {@link PRETTIER_MARKDOWN} is part of what is expected.
|
|
167
|
+
* @returns {Array<{key: string, shared: unknown, local: unknown}>} One entry per
|
|
168
|
+
* shared key the resolved configuration does not carry the value of, in the
|
|
169
|
+
* order {@link PRETTIER_BASE} declares them. `local` is `undefined` where the
|
|
170
|
+
* key is absent entirely, which is not the same finding as a key set to
|
|
171
|
+
* something else and is reported differently.
|
|
172
|
+
*/
|
|
173
|
+
export function sharedPrettierDivergence(resolved, file) {
|
|
174
|
+
return Object.entries(sharedPrettierOptionsFor(file))
|
|
175
|
+
.filter(([key, shared]) => resolved?.[key] !== shared)
|
|
176
|
+
.map(([key, shared]) => ({ key, shared, local: resolved?.[key] }));
|
|
177
|
+
}
|
|
178
|
+
|
|
137
179
|
/**
|
|
138
180
|
* The markdownlint rules — the structural checks Prettier cannot make.
|
|
139
181
|
*
|