@heroiclands/package-build 3.2.0 → 3.3.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 +101 -0
- package/CONTENT.md +29 -4
- package/content-config.mjs +3 -3
- package/engine/base-compiler.mjs +40 -4
- package/engine/content-links.mjs +15 -15
- package/engine/content-package.mjs +18 -5
- package/engine/field-reference.mjs +3 -1
- package/engine/generate.mjs +10 -10
- package/engine/helpers.mjs +14 -6
- package/engine/index.mjs +3 -0
- package/engine/journals.mjs +2 -4
- package/engine/macros.mjs +2 -2
- package/engine/manifest-emit.mjs +10 -5
- package/engine/note-package.mjs +126 -0
- package/engine/pack-router.mjs +3 -2
- package/engine/scenes.mjs +6 -2
- package/engine/site-build.mjs +26 -7
- package/engine/site-index.mjs +8 -1
- package/package.json +1 -1
- package/sohl/actors.mjs +55 -0
- package/sohl/skill-base.mjs +280 -0
- package/types/content-config.d.mts +3 -3
- package/types/engine/base-compiler.d.mts +22 -2
- package/types/engine/content-package.d.mts +18 -5
- package/types/engine/generate.d.mts +3 -3
- package/types/engine/helpers.d.mts +6 -3
- package/types/engine/index.d.mts +1 -0
- package/types/engine/macros.d.mts +2 -2
- package/types/engine/manifest-emit.d.mts +6 -3
- package/types/engine/note-package.d.mts +54 -0
- package/types/engine/pack-router.d.mts +3 -2
- package/types/engine/site-build.d.mts +3 -1
- package/types/sohl/actors.d.mts +27 -0
- package/types/sohl/skill-base.d.mts +53 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,106 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 3.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1f6eb85: Compile an unopened skill's opening mastery level into the actor pack.
|
|
8
|
+
|
|
9
|
+
A skill embedded on a being carries `masteryLevelBase: null` when neither the
|
|
10
|
+
catalogue entry nor the note states one — `null` meaning _not yet opened_. The
|
|
11
|
+
client filled that in on import, at Skill Base × `initSkillMult`
|
|
12
|
+
(`SkillLogic.initialize`), so a compiled being said nothing about what its
|
|
13
|
+
skills open at. It materialised on import instead of being visible in the
|
|
14
|
+
document, reviewable in a diff, or testable without standing up Foundry.
|
|
15
|
+
|
|
16
|
+
The actors pass now computes it. `openUnopenedSkills` runs at the end of
|
|
17
|
+
`buildEmbeddedItems`, after the note's frontmatter has been merged onto the
|
|
18
|
+
catalogue skill and after the attribute items exist — the Skill Base formula
|
|
19
|
+
reads the actor's attributes, so it cannot run any earlier. Only nulls are
|
|
20
|
+
filled; a skill that states a `masteryLevelBase` keeps it.
|
|
21
|
+
|
|
22
|
+
Evaluating `skillBaseFormula` needs an expression evaluator, which this package
|
|
23
|
+
had none of. `sohl/skill-base.mjs` reproduces the part of SoHL's
|
|
24
|
+
`SafeExpression` that a Skill Base uses: numeric literals, `attr.<code>` reads
|
|
25
|
+
defaulting to `0`, arithmetic, and a small helper set including `sb()` — whose
|
|
26
|
+
rounding (a pair averages up only when the primary is the greater) and whose
|
|
27
|
+
`Math.max(0, n)` clamp are copied from SoHL deliberately and must not drift. A
|
|
28
|
+
formula outside that subset is reported, never guessed at.
|
|
29
|
+
|
|
30
|
+
Two build-side rules the client does not need, neither of which changes what a
|
|
31
|
+
client computes:
|
|
32
|
+
|
|
33
|
+
- A zero or absent `initSkillMult` leaves `masteryLevelBase` null. The
|
|
34
|
+
multiplier is the switch for whether a skill opens at all, so writing the `0`
|
|
35
|
+
the arithmetic yields would claim the skill opened at zero rather than that it
|
|
36
|
+
never opened.
|
|
37
|
+
- A fractional product is an error, not a rounding. `masteryLevelBase` is an
|
|
38
|
+
integer field, so there is no honest value to write — the same stance
|
|
39
|
+
`resolveSkillAptitudes` takes on a fractional modifier.
|
|
40
|
+
|
|
41
|
+
The scores used are the `scoreBase` values the pass just wrote, where the client
|
|
42
|
+
resolves `attr.<code>` to an attribute's _effective_ score. They agree for a
|
|
43
|
+
being carrying no attribute-altering effects, which is every being in content
|
|
44
|
+
today; one that did carry such an effect would bake a Skill Base its client then
|
|
45
|
+
disagrees with.
|
|
46
|
+
|
|
47
|
+
Closes #46.
|
|
48
|
+
- e37d201: A note's package is the repository's `contentPackage`, and `package:` is
|
|
49
|
+
optional.
|
|
50
|
+
|
|
51
|
+
A note was compiled when its `package:` frontmatter matched the configured
|
|
52
|
+
`contentPackage`, and skipped when it did not — silently, and tallied as
|
|
53
|
+
`skippedOther`, the same bucket as the thousands of notes that legitimately
|
|
54
|
+
belong to another pass. So a tree whose notes named a package no configuration
|
|
55
|
+
answered to compiled **zero notes and exited 0**, which is the state the
|
|
56
|
+
un-migrated `hm-loc-*` / `hm-adv-*` repositories are in today.
|
|
57
|
+
|
|
58
|
+
Every content tree is single-package — each is single-sourced in the repository
|
|
59
|
+
that ships it — so the field restated one constant about 6,200 times across the
|
|
60
|
+
org. This is the first of three steps that retire it, and the only one that
|
|
61
|
+
changes any code here:
|
|
62
|
+
|
|
63
|
+
1. **Optional**, now. An absent `package:` is normal and the note compiles; a
|
|
64
|
+
present one is accepted while it agrees with `contentPackage`, and is a
|
|
65
|
+
**loud error naming the file** when it does not. Nothing a consumer authors
|
|
66
|
+
has to change, which is what makes the sweep safe to land next.
|
|
67
|
+
2. **Swept** out of every content tree, on this version.
|
|
68
|
+
3. **Rejected** outright — a later major, once the sweeps have merged.
|
|
69
|
+
|
|
70
|
+
**What changed**
|
|
71
|
+
|
|
72
|
+
- `engine/note-package.mjs` is the new seam: `notePackage` derives the package
|
|
73
|
+
a note belongs to, `assertNotePackage` refuses one that names another, and
|
|
74
|
+
`searchableFrontmatter` presents a note to a generated table with its package
|
|
75
|
+
present however the note spells it.
|
|
76
|
+
- The compile loop no longer filters on the field. A note declaring another
|
|
77
|
+
package is reported through the ordinary diagnostic channel
|
|
78
|
+
(`file:line:column: error: …`), counted in `errorCount` so the build fails,
|
|
79
|
+
and tallied as its own `PassStats.declined` — never folded into
|
|
80
|
+
`skippedOther`, which is what made the original defect invisible.
|
|
81
|
+
- The link manifest throws rather than skipping such a note: skipping it
|
|
82
|
+
quietly is how a manifest came to claim a package publishes nothing.
|
|
83
|
+
- **No key is derived from frontmatter any more.** The link index
|
|
84
|
+
(`content-links`), the site index (`site-index`) and the site build's
|
|
85
|
+
package grouping all take the derived value; the manifest emitter already
|
|
86
|
+
took it from configuration. A note addresses identically whether or not it
|
|
87
|
+
declares the field.
|
|
88
|
+
- A generated table that scopes itself with `WHERE … and package = "<pkg>"` —
|
|
89
|
+
the shape every collection note uses — keeps matching after the field is
|
|
90
|
+
deleted, so a sweep is a mechanical deletion rather than a silent
|
|
91
|
+
emptying of every table.
|
|
92
|
+
|
|
93
|
+
**`contentPackage` is not becoming dead configuration.** Its selecting job is
|
|
94
|
+
what is going; the value is the **address namespace** — the first segment of
|
|
95
|
+
every canonical key, the name of the link manifest a build emits, and the
|
|
96
|
+
package a cross-package wikilink writes. Its documentation now says so.
|
|
97
|
+
|
|
98
|
+
Verified against the real `Song-of-Heroic-Lands-FoundryVTT` tree (1,606 notes):
|
|
99
|
+
`build/packs-json` is byte-identical to `main`'s output both with the field
|
|
100
|
+
present on every note and with it deleted from every note.
|
|
101
|
+
|
|
102
|
+
Step 1 of #56. Steps 2 (the sweep) and 3 (rejection, a major) follow.
|
|
103
|
+
|
|
3
104
|
## 3.2.0
|
|
4
105
|
|
|
5
106
|
### Minor Changes
|
package/CONTENT.md
CHANGED
|
@@ -21,7 +21,9 @@ npm install -D @heroiclands/package-build
|
|
|
21
21
|
A consuming repository declares one `package-build.config.yaml` at its root:
|
|
22
22
|
|
|
23
23
|
```yaml
|
|
24
|
-
# The
|
|
24
|
+
# The package this repository's content is published as: the first segment of
|
|
25
|
+
# every canonical address, the name of the link manifest it emits, and the
|
|
26
|
+
# package a cross-package wikilink writes to reach one of its notes.
|
|
25
27
|
contentPackage: thalorna
|
|
26
28
|
# Where Foundry installs it: "systems" or "modules". Also decides the served
|
|
27
29
|
# asset root a note's `img:` resolves to — `modules/sohl-thalorna/assets/…`.
|
|
@@ -173,6 +175,31 @@ never existed. A module declaring no usable system relationship fails the build
|
|
|
173
175
|
rather than guessing — a wrong `_stats.systemVersion` is invisible until
|
|
174
176
|
something migrates on it.
|
|
175
177
|
|
|
178
|
+
### A note's package is the repository's, not the note's
|
|
179
|
+
|
|
180
|
+
`contentPackage` is the **address namespace** every note in the tree is
|
|
181
|
+
published under. It is not a filter, and a note does not restate it.
|
|
182
|
+
|
|
183
|
+
A note may still carry `package:` — every note written before this did — and it
|
|
184
|
+
is accepted while it **agrees** with `contentPackage`. One that disagrees is an
|
|
185
|
+
error naming the file, in the compile, in the link manifest, and anywhere else
|
|
186
|
+
a note is read. The field is redundant, and it is being retired in three steps:
|
|
187
|
+
optional now, swept out of every content tree on this version, and rejected
|
|
188
|
+
outright in a later major.
|
|
189
|
+
|
|
190
|
+
A generated table that scopes itself with `WHERE … and package = "<pkg>"` keeps
|
|
191
|
+
working either way: a note's package is supplied to the table search whether or
|
|
192
|
+
not the note declares it, so deleting the field never turns a table into an
|
|
193
|
+
empty one.
|
|
194
|
+
|
|
195
|
+
It used to **select**: a note compiled when its `package:` matched and was
|
|
196
|
+
skipped when it did not. Every content tree is single-package — each is
|
|
197
|
+
single-sourced in the repository that ships it — so the field restated one
|
|
198
|
+
constant thousands of times, while a tree whose notes named a package no
|
|
199
|
+
configuration answered to compiled **zero notes and exited 0**. Deleting the
|
|
200
|
+
field from a note is safe on this version and is the fix; deleting the
|
|
201
|
+
_configured_ value is not, since every address derives from it.
|
|
202
|
+
|
|
176
203
|
### A registry of your own
|
|
177
204
|
|
|
178
205
|
`itemBuilders` is the one part of the contract that is code — a table of
|
|
@@ -240,7 +267,6 @@ packs:
|
|
|
240
267
|
name:
|
|
241
268
|
full: Climbing
|
|
242
269
|
type: skill
|
|
243
|
-
package: kethira
|
|
244
270
|
id: ...
|
|
245
271
|
---
|
|
246
272
|
# A note that names one lands there instead.
|
|
@@ -248,7 +274,6 @@ id: ...
|
|
|
248
274
|
name:
|
|
249
275
|
full: Second Sight
|
|
250
276
|
type: skill
|
|
251
|
-
package: kethira
|
|
252
277
|
id: ...
|
|
253
278
|
pack: mysteries
|
|
254
279
|
---
|
|
@@ -543,7 +568,7 @@ It reads its whole input from configuration and takes nothing else:
|
|
|
543
568
|
|
|
544
569
|
| Setting | What it decides |
|
|
545
570
|
| --------------------------- | ------------------------------------------------------------- |
|
|
546
|
-
| `contentPackage` | The package emitted,
|
|
571
|
+
| `contentPackage` | The package emitted, which every note belongs to. |
|
|
547
572
|
| `foundryPackage` | The package every emitted `uuid` names. |
|
|
548
573
|
| `paths.content` | The tree walked. |
|
|
549
574
|
| `paths.manifestOut` | Where the file lands (`build/manifests` by default). |
|
package/content-config.mjs
CHANGED
|
@@ -414,9 +414,9 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
414
414
|
* repository — every configured path is
|
|
415
415
|
* resolved against it, so the build never
|
|
416
416
|
* depends on the working directory.
|
|
417
|
-
* @property {string} contentPackage Content package name — the
|
|
418
|
-
*
|
|
419
|
-
*
|
|
417
|
+
* @property {string} contentPackage Content package name — the address
|
|
418
|
+
* namespace every note in this
|
|
419
|
+
* repository is published under.
|
|
420
420
|
* @property {string} foundryPackage Foundry package id, as it appears in
|
|
421
421
|
* `system.json` / `module.json`.
|
|
422
422
|
* @property {PackageKind} packageKind Whether the package is a system or a module.
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -74,16 +74,25 @@ import {
|
|
|
74
74
|
} from "./helpers.mjs";
|
|
75
75
|
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
76
76
|
import { contentPackage } from "./content-package.mjs";
|
|
77
|
+
import { assertNotePackage } from "./note-package.mjs";
|
|
77
78
|
import { assertTypeNotRetired, packForType } from "./ids.mjs";
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
81
|
* The tallies one pass accumulates while walking the tree.
|
|
81
82
|
*
|
|
83
|
+
* `declined` and `skippedOther` are deliberately separate numbers. A declined
|
|
84
|
+
* note is one this build **refused** — it names a package this repository does
|
|
85
|
+
* not compile — and it is an error; a skipped one legitimately belongs to
|
|
86
|
+
* another pass, and there are thousands of those. Folding the first into the
|
|
87
|
+
* second is what let a whole tree be filtered out in silence (#56).
|
|
88
|
+
*
|
|
82
89
|
* @typedef {object} PassStats
|
|
83
90
|
* @property {number} compiled - Notes that became a document.
|
|
84
91
|
* @property {number} skippedDraft - Notes marked `draft: true`.
|
|
85
92
|
* @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
|
|
86
93
|
* @property {number} skippedOther - Notes this pass does not claim.
|
|
94
|
+
* @property {number} declined - Notes refused because they declare another
|
|
95
|
+
* package. Counted as errors, never as skips.
|
|
87
96
|
*/
|
|
88
97
|
|
|
89
98
|
/**
|
|
@@ -250,8 +259,9 @@ export class BasePackCompiler {
|
|
|
250
259
|
/**
|
|
251
260
|
* Whether this pass claims a note. **Required.**
|
|
252
261
|
*
|
|
253
|
-
* Called only for a note
|
|
254
|
-
*
|
|
262
|
+
* Called only for a note this build compiles — every note in the tree
|
|
263
|
+
* belongs to the configured content package (#56) — so a subclass decides
|
|
264
|
+
* on `type` alone.
|
|
255
265
|
*
|
|
256
266
|
* @param {object} fm - The note's frontmatter.
|
|
257
267
|
* @returns {boolean} True to compile it.
|
|
@@ -326,7 +336,9 @@ export class BasePackCompiler {
|
|
|
326
336
|
const { markdown: tabulated, lineMap } = expandNoteTables(body, {
|
|
327
337
|
docs: this.contentDocs,
|
|
328
338
|
name,
|
|
329
|
-
|
|
339
|
+
// The repository's package, not the note's: every note in the tree
|
|
340
|
+
// is this package's note, whether or not it says so (#56).
|
|
341
|
+
pkg: contentPackage(),
|
|
330
342
|
fm,
|
|
331
343
|
bodyLine,
|
|
332
344
|
});
|
|
@@ -503,6 +515,14 @@ export class BasePackCompiler {
|
|
|
503
515
|
if (stats.skippedDraft) {
|
|
504
516
|
log.info(`Skipped ${stats.skippedDraft} draft(s)`);
|
|
505
517
|
}
|
|
518
|
+
if (stats.declined) {
|
|
519
|
+
// Its own line, at error level: these are not skips, and burying
|
|
520
|
+
// them in the skipped tally is the defect (#56). Each one has
|
|
521
|
+
// already been named individually as a diagnostic.
|
|
522
|
+
log.error(
|
|
523
|
+
`Declined ${stats.declined} note(s) declaring another package`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
506
526
|
this.reportDetail(stats);
|
|
507
527
|
}
|
|
508
528
|
|
|
@@ -518,6 +538,7 @@ export class BasePackCompiler {
|
|
|
518
538
|
skippedDraft: 0,
|
|
519
539
|
skippedNoId: 0,
|
|
520
540
|
skippedOther: 0,
|
|
541
|
+
declined: 0,
|
|
521
542
|
};
|
|
522
543
|
await this.prepare();
|
|
523
544
|
|
|
@@ -534,10 +555,25 @@ export class BasePackCompiler {
|
|
|
534
555
|
// Which note this pass is on, so anything it calls can report a
|
|
535
556
|
// position without every method having to be handed one (#17).
|
|
536
557
|
this.currentNote = { absPath, bodyLine, bodyColumn };
|
|
537
|
-
|
|
558
|
+
// A file carrying no frontmatter at all is not a note.
|
|
559
|
+
if (!fm) {
|
|
538
560
|
stats.skippedOther++;
|
|
539
561
|
continue;
|
|
540
562
|
}
|
|
563
|
+
// The package a note belongs to is this repository's configured
|
|
564
|
+
// one; `package:` is optional and merely has to agree (#56). A
|
|
565
|
+
// disagreement is reported and counted as an error — never skipped,
|
|
566
|
+
// which is how a tree naming a package nothing answers to used to
|
|
567
|
+
// compile zero notes and exit 0. The file comes from the diagnostic
|
|
568
|
+
// locator, so the message must not repeat it.
|
|
569
|
+
try {
|
|
570
|
+
assertNotePackage(fm);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
stats.declined++;
|
|
573
|
+
this.errorCount++;
|
|
574
|
+
this.noteError(err.message);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
541
577
|
// Checked before `selects`, and therefore for every note this
|
|
542
578
|
// package owns rather than only the ones some pass claims. A
|
|
543
579
|
// retired type is claimed by no pass, so the alternative is not a
|
package/engine/content-links.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
|
52
52
|
import { expandContentTables } from "./content-tables.mjs";
|
|
53
53
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
54
54
|
import { hasDocEntry } from "./item-docs.mjs";
|
|
55
|
+
import { notePackage, searchableFrontmatter } from "./note-package.mjs";
|
|
55
56
|
import {
|
|
56
57
|
canonicalKey,
|
|
57
58
|
loadForeignManifests,
|
|
@@ -131,21 +132,18 @@ export function buildLinkIndex(
|
|
|
131
132
|
|
|
132
133
|
for (const note of notes) {
|
|
133
134
|
const { fm, type } = note;
|
|
135
|
+
// Derived, never read out of frontmatter: `package:` is optional, and a
|
|
136
|
+
// note that declares nothing addresses exactly as one that declares the
|
|
137
|
+
// configured package (#56).
|
|
138
|
+
const pkg = notePackage(fm);
|
|
134
139
|
if (typeof fm.shortcode === "string" && fm.shortcode) {
|
|
135
140
|
byKey.set(`${type}/${fm.shortcode}`.toLowerCase(), note);
|
|
136
141
|
// The canonical, fully qualified address alongside the short one,
|
|
137
142
|
// so a package-qualified link checks the same way a bare one does.
|
|
138
|
-
|
|
139
|
-
byKey.set(canonicalKey(fm.package, type, fm.shortcode), note);
|
|
140
|
-
}
|
|
143
|
+
byKey.set(canonicalKey(pkg, type, fm.shortcode), note);
|
|
141
144
|
if (hasDocEntry(type)) {
|
|
142
145
|
byKey.set(`doc${type}/${fm.shortcode}`.toLowerCase(), note);
|
|
143
|
-
|
|
144
|
-
byKey.set(
|
|
145
|
-
canonicalKey(fm.package, `doc${type}`, fm.shortcode),
|
|
146
|
-
note,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
146
|
+
byKey.set(canonicalKey(pkg, `doc${type}`, fm.shortcode), note);
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
const aliases = [
|
|
@@ -172,9 +170,7 @@ export function buildLinkIndex(
|
|
|
172
170
|
// A foreign package may use a type this tree has never seen, so its types
|
|
173
171
|
// join `types` — otherwise `readQualifier` reads the link as prose and it
|
|
174
172
|
// is never checked at all.
|
|
175
|
-
const localPackages = new Set(
|
|
176
|
-
notes.map((n) => n.fm?.package).filter(Boolean),
|
|
177
|
-
);
|
|
173
|
+
const localPackages = new Set(notes.map((n) => notePackage(n.fm)));
|
|
178
174
|
const foreign =
|
|
179
175
|
manifestDir ?
|
|
180
176
|
loadForeignManifests(manifestDir, localPackages)
|
|
@@ -182,13 +178,15 @@ export function buildLinkIndex(
|
|
|
182
178
|
for (const v of foreign.index.values()) if (v.type) types.add(v.type);
|
|
183
179
|
|
|
184
180
|
const packages = new Set([
|
|
185
|
-
...[...byKey.values()].map((n) => n.fm
|
|
181
|
+
...[...byKey.values()].map((n) => notePackage(n.fm)),
|
|
186
182
|
...foreign.packages,
|
|
187
183
|
]);
|
|
188
184
|
|
|
189
185
|
/** The searchable universe a `dataview` table draws its rows from. */
|
|
190
186
|
const tableDocs = notes.map((n) => ({
|
|
191
|
-
|
|
187
|
+
// Package present however the note spells it — see
|
|
188
|
+
// {@link searchableFrontmatter} (#56).
|
|
189
|
+
fm: searchableFrontmatter(n.fm),
|
|
192
190
|
path: n.rel,
|
|
193
191
|
tld: n.rel.split("/")[0],
|
|
194
192
|
folder: path.dirname(n.rel).split("/").pop(),
|
|
@@ -207,7 +205,9 @@ export function buildLinkIndex(
|
|
|
207
205
|
let body = note.body;
|
|
208
206
|
if (/^[ \t]*(?:`{3,}|~{3,})[ \t]*dataview\b/im.test(body)) {
|
|
209
207
|
body = expandContentTables(body, {
|
|
210
|
-
docs: tableDocs.filter(
|
|
208
|
+
docs: tableDocs.filter(
|
|
209
|
+
(d) => notePackage(d.fm) === notePackage(note.fm),
|
|
210
|
+
),
|
|
211
211
|
linkable: (d) => Boolean(d.fm.shortcode),
|
|
212
212
|
source: note.file,
|
|
213
213
|
}).markdown;
|
|
@@ -25,12 +25,25 @@
|
|
|
25
25
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* The **content** package: the distribution unit
|
|
29
|
-
*
|
|
28
|
+
* The **content** package: the distribution unit this repository's notes belong
|
|
29
|
+
* to, and the **address namespace** every one of them is published under.
|
|
30
|
+
*
|
|
31
|
+
* It is the first segment of every canonical key (`sohl-skill-clmb`), the name
|
|
32
|
+
* of the link manifest this build emits (`sohl.json`), and the package a
|
|
33
|
+
* cross-package wikilink writes to reach one of these notes. So it is the
|
|
34
|
+
* repository's identity in the address space, not a switch — and never dead
|
|
35
|
+
* configuration, whatever else changes.
|
|
36
|
+
*
|
|
37
|
+
* It was also, until #56, a **selector**: a note declared the same value in its
|
|
38
|
+
* `package:` frontmatter and the compilers kept the ones that matched. Every
|
|
39
|
+
* content tree is single-package — each is single-sourced in the repository that
|
|
40
|
+
* ships it — so the field restated this constant once per note while a value
|
|
41
|
+
* that matched nothing filtered the whole tree out in silence. The field is
|
|
42
|
+
* being retired; the value stays, here, where it is declared once.
|
|
30
43
|
*
|
|
31
44
|
* Stable across compilation targets. If this content were ever compiled for a
|
|
32
|
-
* second game system,
|
|
33
|
-
*
|
|
45
|
+
* second game system, it would still be published as `sohl` — only the Foundry
|
|
46
|
+
* package below would differ.
|
|
34
47
|
*
|
|
35
48
|
* An accessor rather than a hoisted constant, so that importing this module
|
|
36
49
|
* needs no configuration (#2).
|
|
@@ -47,7 +60,7 @@ export function contentPackage() {
|
|
|
47
60
|
* compendium UUID the compilers emit.
|
|
48
61
|
*
|
|
49
62
|
* Distinct from {@link contentPackage}, and equal to it only by coincidence
|
|
50
|
-
* here: a note
|
|
63
|
+
* here: a note is published under `sohl` and its documents are addressed as
|
|
51
64
|
* `Compendium.sohl.<pack>.<Type>.<id>`. In `sohl-thalorna` the two differ
|
|
52
65
|
* (`thalorna` vs `sohl-thalorna`), which is why they are separate values rather
|
|
53
66
|
* than one — treating them as interchangeable is what #1498 was.
|
|
@@ -146,7 +146,9 @@ function workedExample(type, fields) {
|
|
|
146
146
|
` full: An Example ${type}`,
|
|
147
147
|
`type: ${type}`,
|
|
148
148
|
"shortcode: xmpl",
|
|
149
|
-
|
|
149
|
+
// No `package:`. A note's package is the repository's configured
|
|
150
|
+
// `contentPackage`, so the field is redundant and is being retired
|
|
151
|
+
// (#56) — and this example is the smallest note that compiles.
|
|
150
152
|
"id: <16-character id>",
|
|
151
153
|
"sohl:",
|
|
152
154
|
" archetype: null",
|
package/engine/generate.mjs
CHANGED
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* into the shipped LevelDB packs) — it is never committed.
|
|
22
22
|
*
|
|
23
23
|
* Each `*` compiler walks the whole content tree and selects its own entries by
|
|
24
|
-
*
|
|
25
|
-
* so routing is directory-agnostic: a file lands in a
|
|
26
|
-
* `type`, not its location. Which packs exist, in what order, and which folder
|
|
24
|
+
* the note's `type` — every note in the tree belongs to this repository's
|
|
25
|
+
* `contentPackage` (#56) — so routing is directory-agnostic: a file lands in a
|
|
26
|
+
* pack because of its `type`, not its location. Which packs exist, in what order, and which folder
|
|
27
27
|
* hierarchy each one loads are all declared in `package-build.config.yaml`;
|
|
28
28
|
* folder files live under the content root and are referenced from entry
|
|
29
29
|
* frontmatter via `sohl.folder: <id>`.
|
|
@@ -196,9 +196,9 @@ async function generatePack(
|
|
|
196
196
|
* The passes that compiled nothing when they were expected to compile
|
|
197
197
|
* something — a build failure, not a quiet no-op.
|
|
198
198
|
*
|
|
199
|
-
* A pack
|
|
200
|
-
*
|
|
201
|
-
*
|
|
199
|
+
* A pack ships blank whenever every note in a full tree was rejected — by a
|
|
200
|
+
* `selects` that claims nothing, or a `pack:` that routes everything elsewhere
|
|
201
|
+
* — and the build then exits 0 (#1502). The empty-tree guard in
|
|
202
202
|
* {@link generatePacksJson} cannot see that: the tree is full, it is the
|
|
203
203
|
* *output* that is empty.
|
|
204
204
|
*
|
|
@@ -212,10 +212,10 @@ export function emptyPassErrors(passes) {
|
|
|
212
212
|
.map(
|
|
213
213
|
(pass) =>
|
|
214
214
|
`Pack "${pass.name}" compiled 0 entries from a non-empty ` +
|
|
215
|
-
`content tree. Every note was rejected — check that the
|
|
216
|
-
`
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
`content tree. Every note was rejected — check that the tree ` +
|
|
216
|
+
`holds notes of the type this pack claims, and that their ` +
|
|
217
|
+
`\`pack:\` routes here, or declare the pack \`mayBeEmpty\` if it ` +
|
|
218
|
+
`genuinely ships nothing.`,
|
|
219
219
|
);
|
|
220
220
|
}
|
|
221
221
|
|
package/engine/helpers.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import log from "loglevel";
|
|
|
36
36
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
37
37
|
import { packRouter } from "./pack-router.mjs";
|
|
38
38
|
import { contentPackage, foundryPackageId } from "./content-package.mjs";
|
|
39
|
+
import { notePackage, searchableFrontmatter } from "./note-package.mjs";
|
|
39
40
|
import { loadForeignManifests, PACKAGE_BASE } from "./kb-manifest.mjs";
|
|
40
41
|
import { buildWikilinkIndex, convertWikilinks } from "./wikilinks.mjs";
|
|
41
42
|
import { expandContentTables } from "./content-tables.mjs";
|
|
@@ -601,7 +602,9 @@ export function collectContentDocs(contentBase) {
|
|
|
601
602
|
if (!fm) continue;
|
|
602
603
|
const segments = path.relative(contentBase, absPath).split(path.sep);
|
|
603
604
|
docs.push({
|
|
604
|
-
|
|
605
|
+
// With its package present whether the note declares one or not, so
|
|
606
|
+
// a `WHERE … package = "…"` query reads the same either way (#56).
|
|
607
|
+
fm: searchableFrontmatter(fm),
|
|
605
608
|
// POSIX-separated and relative to the content root — what a
|
|
606
609
|
// `path:` search term globs, on every platform.
|
|
607
610
|
path: segments.join("/"),
|
|
@@ -633,14 +636,17 @@ const packLinkable = (doc) =>
|
|
|
633
636
|
* Expand the fenced `dataview` tables in one note's markdown, before wikilinks
|
|
634
637
|
* are resolved — so a generated cell may itself be a wikilink.
|
|
635
638
|
*
|
|
636
|
-
* A table searches only notes of the source note's own
|
|
637
|
-
*
|
|
639
|
+
* A table searches only notes of the source note's own package, so a SoHL page
|
|
640
|
+
* never tabulates setting-package content (and vice versa). Each candidate's
|
|
641
|
+
* package is **derived** rather than read out of its frontmatter: `package:` is
|
|
642
|
+
* optional, and comparing a declared value with an absent one would drop every
|
|
643
|
+
* unswept — or every swept — note from the table (#56).
|
|
638
644
|
*
|
|
639
645
|
* @param {string} body - The note's markdown body.
|
|
640
646
|
* @param {object} ctx
|
|
641
647
|
* @param {Array<object>} ctx.docs - From {@link collectContentDocs}.
|
|
642
648
|
* @param {string} ctx.name - The note, for the error message.
|
|
643
|
-
* @param {string} [ctx.pkg] - The source note's
|
|
649
|
+
* @param {string} [ctx.pkg] - The source note's package.
|
|
644
650
|
* @param {object} [ctx.fm] - The source note's frontmatter, which is what a
|
|
645
651
|
* query's `this` reads. Its entry in `docs` supplies the path as well.
|
|
646
652
|
* @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
|
|
@@ -654,10 +660,12 @@ const packLinkable = (doc) =>
|
|
|
654
660
|
* `position`, the directive's own line.
|
|
655
661
|
*/
|
|
656
662
|
export function expandNoteTables(body, { docs, name, pkg, fm, bodyLine }) {
|
|
657
|
-
const scoped = pkg ? docs.filter((d) => d.fm
|
|
663
|
+
const scoped = pkg ? docs.filter((d) => notePackage(d.fm) === pkg) : docs;
|
|
658
664
|
const self =
|
|
659
665
|
fm ?
|
|
660
|
-
(docs.find((d) => d.fm?.id && d.fm.id === fm.id) ?? {
|
|
666
|
+
(docs.find((d) => d.fm?.id && d.fm.id === fm.id) ?? {
|
|
667
|
+
fm: searchableFrontmatter(fm),
|
|
668
|
+
})
|
|
661
669
|
: undefined;
|
|
662
670
|
const { markdown, errors, lineMap } = expandContentTables(body ?? "", {
|
|
663
671
|
docs: scoped,
|
package/engine/index.mjs
CHANGED
|
@@ -54,6 +54,9 @@ export * as packRouter from "./pack-router.mjs";
|
|
|
54
54
|
/** The content package a build compiles, and the Foundry package it ships in. */
|
|
55
55
|
export * as contentPackage from "./content-package.mjs";
|
|
56
56
|
|
|
57
|
+
/** Which content package a note belongs to, and refusing one that disagrees. */
|
|
58
|
+
export * as notePackage from "./note-package.mjs";
|
|
59
|
+
|
|
57
60
|
/** The shipped Foundry manifest: locating it, reading it, guarding its id. */
|
|
58
61
|
|
|
59
62
|
/** The URL a content note is published at — the one web-address rule. */
|
package/engine/journals.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Foundry compendium from markdown notes in the `assets/content/` tree.
|
|
17
17
|
*
|
|
18
18
|
* The content root (`contentBase`) is walked recursively; any `.md` file
|
|
19
|
-
* whose frontmatter declares
|
|
19
|
+
* whose frontmatter declares either `type: doc` or a
|
|
20
20
|
* **doc-carrying type** ({@link sohl.utils.packs.docEntryTypes} — every item
|
|
21
21
|
* type, plus `macro`) is compiled into one JournalEntry document. Each note's
|
|
22
22
|
* body is split on top-level H1 headings; the optional content before the
|
|
@@ -54,7 +54,6 @@ import {
|
|
|
54
54
|
md,
|
|
55
55
|
} from "./helpers.mjs";
|
|
56
56
|
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
57
|
-
import { contentPackage } from "./content-package.mjs";
|
|
58
57
|
import { anchorPageId } from "./wikilinks.mjs";
|
|
59
58
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
60
59
|
|
|
@@ -367,8 +366,7 @@ export class Journals extends BasePackCompiler {
|
|
|
367
366
|
/** @inheritdoc */
|
|
368
367
|
reportDetail(stats) {
|
|
369
368
|
log.debug(
|
|
370
|
-
`Skipped ${stats.skippedOther} non-doc file(s)
|
|
371
|
-
`(not type:doc package:${contentPackage()})`,
|
|
369
|
+
`Skipped ${stats.skippedOther} non-doc file(s) (not type:doc)`,
|
|
372
370
|
);
|
|
373
371
|
}
|
|
374
372
|
}
|
package/engine/macros.mjs
CHANGED
|
@@ -286,8 +286,8 @@ export function buildMacroEntry(
|
|
|
286
286
|
/**
|
|
287
287
|
* Macros pack compiler.
|
|
288
288
|
*
|
|
289
|
-
* Walks the content tree and compiles every `
|
|
290
|
-
*
|
|
289
|
+
* Walks the content tree and compiles every `type: macro` note into one Macro
|
|
290
|
+
* document. The same note's documentation is compiled by
|
|
291
291
|
* the journals pass; neither pass reads the other's output.
|
|
292
292
|
*/
|
|
293
293
|
export class Macros extends BasePackCompiler {
|
package/engine/manifest-emit.mjs
CHANGED
|
@@ -54,6 +54,7 @@ import { canonicalKey, writeManifests } from "./kb-manifest.mjs";
|
|
|
54
54
|
import { walkMarkdownTree } from "./helpers.mjs";
|
|
55
55
|
import { compendiumUuid, packForType, pageUuid } from "./ids.mjs";
|
|
56
56
|
import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
|
|
57
|
+
import { assertNotePackage } from "./note-package.mjs";
|
|
57
58
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
58
59
|
import { routerFor } from "./pack-router.mjs";
|
|
59
60
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
@@ -194,9 +195,12 @@ export function entriesForNote(fm, name, address, body, ctx) {
|
|
|
194
195
|
*
|
|
195
196
|
* Drafts are excluded because the site does not publish them, and an entry for
|
|
196
197
|
* an unpublished page is exactly the dead link the manifest exists to prevent.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
198
|
+
*
|
|
199
|
+
* Every note in the tree is this package's note, whether or not it says so:
|
|
200
|
+
* `package:` is optional and merely has to agree (#56). A note naming a
|
|
201
|
+
* different package **throws** rather than being skipped — this build is not
|
|
202
|
+
* authoritative for it, and skipping it silently is how a whole tree came to be
|
|
203
|
+
* filtered out of a manifest that then claimed the package published nothing.
|
|
200
204
|
*
|
|
201
205
|
* A note that has no address is **reported, not guessed** — the finding carries
|
|
202
206
|
* the file and the reason, so a caller can print it or fail on it. Inventing an
|
|
@@ -220,11 +224,12 @@ export function collectManifestEntries(contentBase, ctx) {
|
|
|
220
224
|
contentBase,
|
|
221
225
|
{ skipDirectories: ctx.skipDirectories },
|
|
222
226
|
)) {
|
|
223
|
-
if (!fm
|
|
227
|
+
if (!fm) continue;
|
|
228
|
+
const rel = path.relative(contentBase, absPath);
|
|
229
|
+
assertNotePackage(fm, { file: rel, configured: ctx.contentPackage });
|
|
224
230
|
if (fm.draft === true) continue;
|
|
225
231
|
if (!fm.type || !fm.shortcode) continue;
|
|
226
232
|
|
|
227
|
-
const rel = path.relative(contentBase, absPath);
|
|
228
233
|
const base = path.basename(absPath);
|
|
229
234
|
const name = fm.name?.full ?? path.basename(absPath, ".md");
|
|
230
235
|
|