@heroiclands/package-build 0.6.1 → 3.0.1
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-content-build.md +965 -0
- package/CHANGELOG.md +68 -0
- package/CONTENT.md +824 -0
- package/MIGRATING.md +127 -0
- package/README.md +29 -17
- package/bin/content-build.mjs +1082 -0
- package/bin/package-build.mjs +2 -2
- package/bin/report.mjs +2 -2
- package/config.mjs +21 -18
- package/content-config.mjs +1424 -0
- package/coverage.mjs +1 -1
- package/e2e.mjs +1 -1
- package/engine/abbreviations.mjs +0 -0
- package/engine/base-compiler.mjs +608 -0
- package/engine/code-fences.mjs +311 -0
- package/engine/compendiums.mjs +353 -0
- package/engine/content-address.mjs +141 -0
- package/engine/content-links.mjs +438 -0
- package/engine/content-lint.mjs +204 -0
- package/engine/content-package.mjs +67 -0
- package/engine/content-slug.mjs +169 -0
- package/engine/content-tables.mjs +1216 -0
- package/engine/content-tree.mjs +56 -0
- package/engine/diagnostics.mjs +270 -0
- package/engine/field-reference.mjs +242 -0
- package/engine/field-spec.mjs +218 -0
- package/engine/foreign-catalog.mjs +450 -0
- package/engine/foreign-manifests.mjs +129 -0
- package/engine/frontmatter-lint.mjs +343 -0
- package/engine/frontmatter.mjs +258 -0
- package/engine/generate.mjs +305 -0
- package/engine/helpers.mjs +809 -0
- package/engine/ids.mjs +192 -0
- package/engine/index.mjs +142 -0
- package/engine/item-docs.mjs +153 -0
- package/engine/item-registry.mjs +129 -0
- package/engine/journals.mjs +374 -0
- package/engine/kb-manifest.mjs +473 -0
- package/engine/macros.mjs +333 -0
- package/engine/manifest-emit.mjs +339 -0
- package/engine/map-notes.mjs +1302 -0
- package/engine/pack-config.mjs +479 -0
- package/engine/pack-router.mjs +254 -0
- package/engine/prose-config.mjs +238 -0
- package/engine/prose-lint.mjs +294 -0
- package/engine/region-events.mjs +79 -0
- package/engine/scene-levels.mjs +420 -0
- package/engine/scenes.mjs +528 -0
- package/engine/site-build.mjs +778 -0
- package/engine/site-index.mjs +332 -0
- package/engine/web-wikilinks.mjs +330 -0
- package/engine/wikilink-syntax.mjs +105 -0
- package/engine/wikilinks.mjs +645 -0
- package/index.mjs +42 -25
- package/lang.mjs +1 -1
- package/manifest.mjs +79 -5
- package/markdownlint-config.mjs +45 -0
- package/package.json +78 -35
- package/prettier-config.mjs +34 -0
- package/sohl/actors.mjs +581 -0
- package/sohl/affiliation-standings.mjs +44 -0
- package/sohl/being-info.mjs +173 -0
- package/sohl/default-item-art.mjs +82 -0
- package/sohl/index.mjs +55 -0
- package/sohl/item-builders.mjs +114 -0
- package/sohl/item-fields.mjs +867 -0
- package/sohl/items.mjs +197 -0
- package/sohl/kb-passes.mjs +207 -0
- package/sohl/note-schemas.mjs +322 -0
- package/templates.mjs +1 -1
- package/types/config.d.mts +1 -1
- package/types/content-config.d.mts +571 -0
- package/types/engine/abbreviations.d.mts +44 -0
- package/types/engine/base-compiler.d.mts +320 -0
- package/types/engine/code-fences.d.mts +102 -0
- package/types/engine/compendiums.d.mts +77 -0
- package/types/engine/content-address.d.mts +61 -0
- package/types/engine/content-links.d.mts +87 -0
- package/types/engine/content-lint.d.mts +47 -0
- package/types/engine/content-package.d.mts +36 -0
- package/types/engine/content-slug.d.mts +59 -0
- package/types/engine/content-tables.d.mts +131 -0
- package/types/engine/content-tree.d.mts +12 -0
- package/types/engine/diagnostics.d.mts +152 -0
- package/types/engine/field-reference.d.mts +20 -0
- package/types/engine/field-spec.d.mts +181 -0
- package/types/engine/foreign-catalog.d.mts +103 -0
- package/types/engine/foreign-manifests.d.mts +43 -0
- package/types/engine/frontmatter-lint.d.mts +74 -0
- package/types/engine/frontmatter.d.mts +99 -0
- package/types/engine/generate.d.mts +56 -0
- package/types/engine/helpers.d.mts +348 -0
- package/types/engine/ids.d.mts +126 -0
- package/types/engine/index.d.mts +33 -0
- package/types/engine/item-docs.d.mts +73 -0
- package/types/engine/item-registry.d.mts +57 -0
- package/types/engine/journals.d.mts +121 -0
- package/types/engine/kb-manifest.d.mts +214 -0
- package/types/engine/macros.d.mts +201 -0
- package/types/engine/manifest-emit.d.mts +137 -0
- package/types/engine/map-notes.d.mts +283 -0
- package/types/engine/pack-config.d.mts +77 -0
- package/types/engine/pack-router.d.mts +53 -0
- package/types/engine/prose-config.d.mts +121 -0
- package/types/engine/prose-lint.d.mts +59 -0
- package/types/engine/region-events.d.mts +43 -0
- package/types/engine/scene-levels.d.mts +36 -0
- package/types/engine/scenes.d.mts +48 -0
- package/types/engine/site-build.d.mts +214 -0
- package/types/engine/site-index.d.mts +141 -0
- package/types/engine/web-wikilinks.d.mts +67 -0
- package/types/engine/wikilink-syntax.d.mts +101 -0
- package/types/engine/wikilinks.d.mts +172 -0
- package/types/index.d.mts +3 -0
- package/types/manifest.d.mts +52 -4
- package/types/markdownlint-config.d.mts +8 -0
- package/types/prettier-config.d.mts +1 -0
- package/types/sohl/actors.d.mts +65 -0
- package/types/sohl/affiliation-standings.d.mts +22 -0
- package/types/sohl/being-info.d.mts +78 -0
- package/types/sohl/default-item-art.d.mts +28 -0
- package/types/sohl/index.d.mts +7 -0
- package/types/sohl/item-builders.d.mts +11 -0
- package/types/sohl/item-fields.d.mts +20 -0
- package/types/sohl/items.d.mts +19 -0
- package/types/sohl/kb-passes.d.mts +71 -0
- package/types/sohl/note-schemas.d.mts +7 -0
package/CONTENT.md
ADDED
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
# @heroiclands/package-build
|
|
2
|
+
|
|
3
|
+
The shared toolchain that compiles a **HeroicLands content tree** — a folder of
|
|
4
|
+
Markdown notes with YAML frontmatter — into **Foundry VTT compendium packs**.
|
|
5
|
+
|
|
6
|
+
Every HeroicLands content module (`sohl`, `thalorna`, `kethira`, and the
|
|
7
|
+
adventure modules) builds its packs from this one implementation, rather than
|
|
8
|
+
from a copied `utils/packs/` tree.
|
|
9
|
+
|
|
10
|
+
It ships a command line as well as a library — see
|
|
11
|
+
[Command line](#command-line) for the whole surface.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
npm install -D @heroiclands/package-build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configure
|
|
20
|
+
|
|
21
|
+
A consuming repository declares one `package-build.config.yaml` at its root:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
# The value each content note carries in its `package:` frontmatter.
|
|
25
|
+
contentPackage: thalorna
|
|
26
|
+
# Where Foundry installs it: "systems" or "modules". Also decides the served
|
|
27
|
+
# asset root a note's `img:` resolves to — `modules/sohl-thalorna/assets/…`.
|
|
28
|
+
packageKind: modules
|
|
29
|
+
|
|
30
|
+
# The Foundry core range this package supports. `minimum` is stamped into every
|
|
31
|
+
# compiled document as `_stats.coreVersion`; `verified` names the newest build
|
|
32
|
+
# the full suite has actually passed on — never an aspiration.
|
|
33
|
+
compatibility:
|
|
34
|
+
minimum: "14.359"
|
|
35
|
+
verified: "14.364"
|
|
36
|
+
|
|
37
|
+
# What this package declares about others, in Foundry's own shape. A module's
|
|
38
|
+
# `_stats.systemVersion` comes from the `verified` version of the system it
|
|
39
|
+
# targets — note that this `compatibility` is the *system's* range, not
|
|
40
|
+
# Foundry's. Same key, different subject.
|
|
41
|
+
relationships:
|
|
42
|
+
systems:
|
|
43
|
+
- id: sohl
|
|
44
|
+
type: system
|
|
45
|
+
manifest: https://github.com/HeroicLands/Song-of-Heroic-Lands-FoundryVTT/releases/latest/download/system.json
|
|
46
|
+
compatibility:
|
|
47
|
+
minimum: "0.4.0"
|
|
48
|
+
verified: "0.4.3"
|
|
49
|
+
|
|
50
|
+
# Stamped into every compiled document's `_stats`. `coreVersion` and
|
|
51
|
+
# `systemVersion` are both absent on purpose — see the derived table below.
|
|
52
|
+
stats:
|
|
53
|
+
systemId: sohl
|
|
54
|
+
lastModifiedBy: thalornabuild000
|
|
55
|
+
|
|
56
|
+
# Which content types compile into Items, and what builds each one's `system`
|
|
57
|
+
# block — named, because the registry is code. The registry's keys are the
|
|
58
|
+
# accepted item types, so a type cannot be whitelisted without a builder behind
|
|
59
|
+
# it. A module that ships no items omits this key. See "An item type's default
|
|
60
|
+
# art" below, and "A registry of your own" for the `.mjs` form.
|
|
61
|
+
itemBuilders: sohl
|
|
62
|
+
|
|
63
|
+
# Directory names the content walk ignores wherever they appear.
|
|
64
|
+
skipDirectories: [Templates]
|
|
65
|
+
|
|
66
|
+
# Optional; each path is relative to this file's directory and defaults to the
|
|
67
|
+
# conventional layout shown here.
|
|
68
|
+
paths:
|
|
69
|
+
content: assets/content
|
|
70
|
+
# Vendored foreign manifests, read by `links`. Inbound.
|
|
71
|
+
manifests: assets/manifests
|
|
72
|
+
# Where `manifest` writes this package's own. Outbound, and a build artifact.
|
|
73
|
+
manifestOut: build/manifests
|
|
74
|
+
packJson: build/packs-json
|
|
75
|
+
stage: build/stage/packs
|
|
76
|
+
unpack: build/tmp/packs
|
|
77
|
+
|
|
78
|
+
# The one pack list. Order is load-bearing where one pass reads another's
|
|
79
|
+
# output, and `packDirectories` is derived from it.
|
|
80
|
+
packs:
|
|
81
|
+
- { name: items, type: Item, label: Items, folders: item-folders.yaml }
|
|
82
|
+
- { name: journals, type: JournalEntry, label: Journals }
|
|
83
|
+
# A companion is written by its parent's pass rather than one of its own.
|
|
84
|
+
- name: scenes
|
|
85
|
+
type: Scene
|
|
86
|
+
companions:
|
|
87
|
+
- { name: adventures, type: Adventure }
|
|
88
|
+
|
|
89
|
+
# How this repository frames the pages `content-build docs` generates. The
|
|
90
|
+
# tables come from the itemBuilders registry and are the same everywhere; the
|
|
91
|
+
# heading, the filing and what a reader is told first are this repository's.
|
|
92
|
+
docs:
|
|
93
|
+
itemFields:
|
|
94
|
+
title: Item Note Frontmatter
|
|
95
|
+
out: kb/dev-docs/content-creator/item-frontmatter.md
|
|
96
|
+
preamble:
|
|
97
|
+
- "See also: [The Authoring Workflow](authoring-workflow.md)"
|
|
98
|
+
- ""
|
|
99
|
+
- Every item note carries the frontmatter envelope described there. This
|
|
100
|
+
page covers what each **type** adds to it.
|
|
101
|
+
|
|
102
|
+
# Reserved for @heroiclands/package-build, which validates what is inside it.
|
|
103
|
+
# One repository describes itself in one file; the two build packages split by
|
|
104
|
+
# input, and neither learns the other's schema. Values package-build needs that
|
|
105
|
+
# already live at the top level — `packageKind`, `foundryPackage` — it reads
|
|
106
|
+
# from there rather than restating them here.
|
|
107
|
+
packageBuild:
|
|
108
|
+
assets:
|
|
109
|
+
- { from: assets/icons, to: assets/icons }
|
|
110
|
+
|
|
111
|
+
# Three independent switches — every combination is real — plus the address
|
|
112
|
+
# scheme both `manifest` and `site` derive addresses under.
|
|
113
|
+
publish:
|
|
114
|
+
site: true
|
|
115
|
+
manifests: { publish: true, consume: true }
|
|
116
|
+
address:
|
|
117
|
+
prefix: kb/
|
|
118
|
+
landing: readme
|
|
119
|
+
|
|
120
|
+
# How this repository frames the website `content-build site` publishes.
|
|
121
|
+
# Framing only: addresses come from `publish.address` above.
|
|
122
|
+
site:
|
|
123
|
+
out: kb/content
|
|
124
|
+
landing: { title: Knowledgebase, type: knowledgebase }
|
|
125
|
+
sections:
|
|
126
|
+
being: { title: Beings, banner: banners/creature.webp }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The loader validates the document, resolves every path against the directory
|
|
130
|
+
the file sits in, fills the optional halves with their defaults
|
|
131
|
+
(`skipDirectories: []`, `packageBuild: {}`, the conventional `paths`, every
|
|
132
|
+
publishing switch off),
|
|
133
|
+
derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
|
|
134
|
+
freezes the result. A malformed configuration throws a `TypeError` naming the
|
|
135
|
+
offending field, so it fails at load rather than as an empty pack much later.
|
|
136
|
+
|
|
137
|
+
**Four values are derived rather than authored**, because each is something a
|
|
138
|
+
file can be asked for rather than told:
|
|
139
|
+
|
|
140
|
+
| Field | Derived from |
|
|
141
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `rootDir` | the directory the configuration file sits in |
|
|
143
|
+
| `foundryPackage` | the `name` of the adjacent `package.json`, verbatim |
|
|
144
|
+
| `stats.systemVersion` | a **system**: that `package.json`'s `version`. A **module**: the `verified` version of the system it declares a relationship with |
|
|
145
|
+
| `itemBuilders` | the named registry (`sohl`), required lazily so importing costs nothing |
|
|
146
|
+
|
|
147
|
+
**Authoring any of the first three is an error**, not an override. Each was
|
|
148
|
+
previously transcribed from a file that already stated it, and a transcription
|
|
149
|
+
is free to disagree with what it copies — `stats.systemVersion` froze at
|
|
150
|
+
`0.6.0` for four releases before anyone noticed, and was still frozen there in
|
|
151
|
+
two repositories afterwards.
|
|
152
|
+
|
|
153
|
+
A module does **not** take its system version from its own `package.json`: that
|
|
154
|
+
is the _module's_ version, and stamping it would claim a system version that
|
|
155
|
+
never existed. A module declaring no usable system relationship fails the build
|
|
156
|
+
rather than guessing — a wrong `_stats.systemVersion` is invisible until
|
|
157
|
+
something migrates on it.
|
|
158
|
+
|
|
159
|
+
### A registry of your own
|
|
160
|
+
|
|
161
|
+
`itemBuilders` is the one part of the contract that is code — a table of
|
|
162
|
+
functions building each type's `system` block — so data can only _name_ one of
|
|
163
|
+
the registries this package ships. A consumer supplying its own writes
|
|
164
|
+
`package-build.config.mjs` instead, which is loaded in place of the YAML:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
import { defineConfig } from "@heroiclands/package-build/content-config";
|
|
168
|
+
import { ITEM_BUILDERS } from "./build/item-builders.mjs";
|
|
169
|
+
|
|
170
|
+
export default defineConfig({
|
|
171
|
+
// Stated, since a code configuration derives nothing: it is code, and can
|
|
172
|
+
// read whatever it likes for itself.
|
|
173
|
+
rootDir: import.meta.dirname,
|
|
174
|
+
contentPackage: "kethira",
|
|
175
|
+
foundryPackage: "sohl-kethira-basic",
|
|
176
|
+
packageKind: "modules",
|
|
177
|
+
compatibility: { minimum: "14.359", verified: "14.364" },
|
|
178
|
+
stats: { systemId: "sohl", systemVersion: "0.4.3", lastModifiedBy: "…" },
|
|
179
|
+
itemBuilders: ITEM_BUILDERS,
|
|
180
|
+
packs: [{ name: "items", type: "Item" }],
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The two forms end at the same `defineConfig`, so they are validated and frozen
|
|
185
|
+
identically; a code config simply states the three fields above itself, which it
|
|
186
|
+
can, because it is code. **Import `defineConfig` from
|
|
187
|
+
`@heroiclands/package-build/content-config`, never from the package root** — the root
|
|
188
|
+
barrel pulls in the compilers, the compilers read the resolved configuration,
|
|
189
|
+
and resolving it loads this file, so importing the barrel here closes a cycle
|
|
190
|
+
around the file's own evaluation. The `/config` entry point imports nothing but
|
|
191
|
+
`node:path` and the id helpers, so it cannot.
|
|
192
|
+
|
|
193
|
+
**One directory, one configuration.** A directory holding both a `.yaml` and an
|
|
194
|
+
`.mjs` is an error, not a precedence question: picking one would let a
|
|
195
|
+
repository mid-conversion build from the file its author is no longer editing,
|
|
196
|
+
and look entirely healthy doing it.
|
|
197
|
+
|
|
198
|
+
### Several packs of one document type
|
|
199
|
+
|
|
200
|
+
A repository may declare more than one pack of the same `type`, and route notes
|
|
201
|
+
between them. Editorial grouping of same-type documents into separate
|
|
202
|
+
compendiums is ordinary Foundry practice — "Core Spells" and "Expanded Spells"
|
|
203
|
+
are two Item packs — and it matters beyond taste: a compendium UUID carries its
|
|
204
|
+
pack name (`Compendium.<package>.<pack>.Item.<id>`), so collapsing several packs
|
|
205
|
+
into one invalidates every reference an existing world holds.
|
|
206
|
+
|
|
207
|
+
Two axes, deliberately orthogonal:
|
|
208
|
+
|
|
209
|
+
- a pack's **`type`** selects the _compiler_ that fills it;
|
|
210
|
+
- a note's **`pack:`** frontmatter selects _which pack of that type_ receives its
|
|
211
|
+
document.
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
packs:
|
|
215
|
+
- { name: characteristics, type: Item, default: true }
|
|
216
|
+
- { name: mysteries, type: Item }
|
|
217
|
+
- { name: journals, type: JournalEntry }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
# A note that says nothing lands in `characteristics`, the default Item pack.
|
|
222
|
+
---
|
|
223
|
+
name:
|
|
224
|
+
full: Climbing
|
|
225
|
+
type: skill
|
|
226
|
+
package: kethira
|
|
227
|
+
id: ...
|
|
228
|
+
---
|
|
229
|
+
# A note that names one lands there instead.
|
|
230
|
+
---
|
|
231
|
+
name:
|
|
232
|
+
full: Second Sight
|
|
233
|
+
type: skill
|
|
234
|
+
package: kethira
|
|
235
|
+
id: ...
|
|
236
|
+
pack: mysteries
|
|
237
|
+
---
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- **`pack:` is optional, and silence means the default.** Every note written
|
|
241
|
+
before this existed declares nothing, so an undeclared note must keep
|
|
242
|
+
compiling exactly where it always did. A type with exactly **one** pack is
|
|
243
|
+
that type's default implicitly; a type with several designates one with
|
|
244
|
+
`default: true`. Where several exist and none is marked, a declaration is
|
|
245
|
+
**mandatory** and an undeclared note fails the build.
|
|
246
|
+
- **A `pack:` naming no configured pack is a build error**, not a fall-through to
|
|
247
|
+
the default. A typo'd name that quietly landed content in the wrong compendium
|
|
248
|
+
would be silent partial compilation — the failure mode this toolchain's guards
|
|
249
|
+
exist to eliminate. The same applies to a name that belongs to a pack of
|
|
250
|
+
another document type, or to a companion (no note is ever routed into one).
|
|
251
|
+
- **A note's `pack:` names where its _own_ document goes.** Anything derived from
|
|
252
|
+
it — an item's or a macro's prose, which compiles into a JournalEntry of its
|
|
253
|
+
own — lands in the default pack of _that_ type.
|
|
254
|
+
|
|
255
|
+
**The configuration is found by walking up, not from the working directory.**
|
|
256
|
+
`engine/pack-config.mjs` climbs from itself — so it works from `packages/` and
|
|
257
|
+
from `node_modules/` alike, and does not depend on the directory the build was
|
|
258
|
+
launched from. Set `PACKAGE_BUILD_CONFIG` to point at the file explicitly if a
|
|
259
|
+
consumer keeps it somewhere else.
|
|
260
|
+
|
|
261
|
+
**The configuration is resolved on first read, never at import.** Every module
|
|
262
|
+
here can be imported — and `content-build --version` and `--help` answered — in a
|
|
263
|
+
directory with no `package-build.config.yaml` and no Foundry package manifest, so
|
|
264
|
+
a consumer can reach for one pure helper (`engine/content-slug`,
|
|
265
|
+
`engine/wikilinks`) without standing up a pack build. Anything derived from
|
|
266
|
+
configuration is therefore an accessor rather than a hoisted constant —
|
|
267
|
+
`loadPackConfig()`, `contentPackage()`, `foundryPackageId()`, `itemTypes()`,
|
|
268
|
+
`docEntryTypes()`, `packRouter()`, `defaultTemplateDir()` — and each throws, with
|
|
269
|
+
the same explicit message as before, the moment a build actually needs a value it
|
|
270
|
+
cannot find. Absence is still a hard failure; only the moment it is reported
|
|
271
|
+
moved (#2).
|
|
272
|
+
|
|
273
|
+
The file is read synchronously — an `.mjs` one with `require` — so that reading
|
|
274
|
+
a configured value stays an ordinary expression instead of making every module
|
|
275
|
+
downstream of it an async one. The one shape that cannot be loaded is an `.mjs`
|
|
276
|
+
config whose own module graph uses top-level `await`, which is reported as such.
|
|
277
|
+
|
|
278
|
+
**`itemBuilders` is how the engine learns a consumer's item types without
|
|
279
|
+
holding its data model.** `itemTypes` is its key set, and `docEntryTypes` — every
|
|
280
|
+
type whose prose compiles into a JournalEntry of its own — is composed from it
|
|
281
|
+
exactly once, here, and read through `loadPackConfig()` everywhere. There is one
|
|
282
|
+
resolved set at runtime; the compilers and the link-manifest emitter cannot come
|
|
283
|
+
to disagree about which notes carry documentation.
|
|
284
|
+
|
|
285
|
+
The Item compiler **dispatches through that same resolved table**, via
|
|
286
|
+
`engine/item-registry.mjs` (`itemTypes()` and `itemBuilder(type)`), so the types a
|
|
287
|
+
consumer's notes are accepted for and the builders they compile with are one
|
|
288
|
+
object. Supplying `itemBuilders` is therefore all a consumer does to define an
|
|
289
|
+
item type of its own; a table this package ships is one possible value, not the
|
|
290
|
+
one the compiler holds.
|
|
291
|
+
|
|
292
|
+
**Configuration is the source, and the manifest is generated from it.** That
|
|
293
|
+
arrow used to point the other way: `paths.packageManifest` said where a
|
|
294
|
+
hand-authored `system.template.json` lived, and the package-id guard and the
|
|
295
|
+
`_stats.coreVersion` stamp both read out of it. Both are gone — the floor is the
|
|
296
|
+
top-level `compatibility.minimum`, the id is derived from `package.json`
|
|
297
|
+
`name`, and `@heroiclands/package-build` writes the manifest from this file.
|
|
298
|
+
|
|
299
|
+
### An item type's default art
|
|
300
|
+
|
|
301
|
+
A note that carries no `img:` gets its type's **default art**, and a type
|
|
302
|
+
declares that art in the same place it declares its builder. An `itemBuilders`
|
|
303
|
+
entry may be written two ways:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
itemBuilders: {
|
|
307
|
+
// A bare builder. Every note of this type must carry its own `img:`.
|
|
308
|
+
charm: buildCharm,
|
|
309
|
+
// The same builder, paired with the art a note of this type gets when it
|
|
310
|
+
// sets no `img:` of its own.
|
|
311
|
+
relic: { system: buildRelic, img: "icons/relic.svg" },
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Both spellings are equal; the difference is only whether the type brings art.
|
|
316
|
+
`itemTypes` is still the key set either way, so a type is still impossible to
|
|
317
|
+
whitelist without a builder behind it.
|
|
318
|
+
|
|
319
|
+
**The path is spelled the way a note spells it.** Registry art goes through the
|
|
320
|
+
same `resolveImg` rule as a note's `img:`, so `icons/relic.svg` means _this_
|
|
321
|
+
repository's asset root — `modules/sohl-relics/assets/icons/relic.svg` — and an
|
|
322
|
+
already-served path (`systems/sohl/assets/icons/…`) passes through untouched.
|
|
323
|
+
|
|
324
|
+
**A type with neither is a build error, deliberately.** When a note sets no
|
|
325
|
+
`img:` and its type pairs none, the pack build aborts rather than shipping an
|
|
326
|
+
item with a mismatched icon:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
No default art for item type "relic" — the note carries no `img:`, and the
|
|
330
|
+
`itemBuilders` entry for "relic" in this repository's configuration
|
|
331
|
+
pairs none with its builder.
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### Why art travels with the builder (#7)
|
|
335
|
+
|
|
336
|
+
It did not always. The item **type** whitelist was derived from a consumer's
|
|
337
|
+
`itemBuilders` keys, while the **art** for those same types was looked up in
|
|
338
|
+
`sohl/default-item-art.mjs` — a table this package ships for the `sohl` package
|
|
339
|
+
and which a consumer cannot add to. A type was therefore configurable while its
|
|
340
|
+
default art was not, and a second consumer's own item type compiled only if
|
|
341
|
+
every one of its notes carried an explicit `img:`; the first note that omitted
|
|
342
|
+
one failed the build with an error naming a module in someone else's package.
|
|
343
|
+
|
|
344
|
+
Widening that map was not the fix. It is deliberately SoHL data, shared with the
|
|
345
|
+
runtime's `SohlItem.getDefaultArtwork` so that the build-time and runtime
|
|
346
|
+
defaults are one list and cannot drift (SoHL#932/#1510). Pairing art with the
|
|
347
|
+
builder instead moves it onto the seam a type is _already_ declared through, and
|
|
348
|
+
costs the `sohl` package nothing: `ITEM_BUILDERS` reads each entry's image out of
|
|
349
|
+
that same map, so there is still exactly one map — and the drift a test used to
|
|
350
|
+
watch for is now unrepresentable, because building the registry throws if a type
|
|
351
|
+
has no art.
|
|
352
|
+
|
|
353
|
+
## Command line
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
npx content-build package <compile|unpack|clean> [pack] [entry]
|
|
357
|
+
npx content-build docs item-fields [--out <path>] [--title <title>]
|
|
358
|
+
npx content-build lint [root] [--no-references]
|
|
359
|
+
npx content-build links [root] [--manifests <dir>]
|
|
360
|
+
npx content-build format [paths..] [--write]
|
|
361
|
+
npx content-build markdown [paths..] [--fix]
|
|
362
|
+
npx content-build manifest [root] [--out <dir>]
|
|
363
|
+
npx content-build site [--out <dir>]
|
|
364
|
+
npx content-build reachability <dir> [file] [--index <shortcode>]
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
| Command | What it does |
|
|
368
|
+
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
369
|
+
| `package` | Compile the content tree into LevelDB packs, unpack a shipped pack back to JSON, or clean one. See [Install](#install). |
|
|
370
|
+
| `docs` | Render a generated reference from the configured registries. `item-fields` is the item-frontmatter page. |
|
|
371
|
+
| `lint` | Check a content tree's addresses and its frontmatter. See [Linting a content tree](#linting-a-content-tree). |
|
|
372
|
+
| `links` | Check that every link in the tree lands: dead anchors, dead qualified addresses, wikilinks in frontmatter, drifted manifests. |
|
|
373
|
+
| `format` | Prettier, with the shared configuration. See [Prose: formatting and markdown](#prose-formatting-and-markdown). |
|
|
374
|
+
| `markdown` | markdownlint, with the shared rule set — the structure Prettier is indifferent to. |
|
|
375
|
+
| `manifest` | Emit this package's cross-package link manifest. See [Publishing a link manifest](#publishing-a-link-manifest). |
|
|
376
|
+
| `site` | Publish the content tree as a website. See [Publishing a website](#publishing-a-website). |
|
|
377
|
+
| `reachability` | Walk outward from an index note and report what no path reaches, for a tree meant to be navigable from one entry point. |
|
|
378
|
+
|
|
379
|
+
Every path, pack name and root it needs comes from the consuming repository's
|
|
380
|
+
`package-build.config.yaml`, so the usual invocation takes no arguments beyond
|
|
381
|
+
the command itself. What may be named on the command line overrides that.
|
|
382
|
+
|
|
383
|
+
**Every invocation it accepts is one it performs.** A missing command, an
|
|
384
|
+
unknown command, a missing or unknown action, and an unknown option are each an
|
|
385
|
+
error that names what was wrong and exits non-zero — never a silent success. A
|
|
386
|
+
build chain can therefore treat a zero exit as "the work happened". `--version`
|
|
387
|
+
and `--help` still answer in a directory with no configuration at all.
|
|
388
|
+
|
|
389
|
+
## Linting a content tree
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
npx content-build lint # the configured `paths.content`
|
|
393
|
+
npx content-build lint some/tree # or a tree named outright
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Checks the two rules every note's **identity** is authored against, and reports
|
|
397
|
+
each finding in the located form below:
|
|
398
|
+
|
|
399
|
+
- **Shape** — a `shortcode` is strictly ASCII-alphanumeric. It is the identity
|
|
400
|
+
key referenced from saved world data, and half of the `type-shortcode`
|
|
401
|
+
address, whose parse needs the separating hyphen to be the only hyphen.
|
|
402
|
+
- **Uniqueness** — `(type, shortcode)` names one note. A document is addressed
|
|
403
|
+
across _every_ pack of its document type, so routing two same-address notes to
|
|
404
|
+
different packs with `pack:` does not separate them.
|
|
405
|
+
|
|
406
|
+
It compiles nothing, opens no LevelDB and needs no Foundry manifest, so it runs
|
|
407
|
+
in about a second and can gate a commit. An empty or untyped tree **fails**
|
|
408
|
+
rather than passing: "every one of nothing is unique" is a vacuous pass, and it
|
|
409
|
+
is exactly what a tree that failed to check out produces.
|
|
410
|
+
|
|
411
|
+
### Frontmatter, against the schema its type declares
|
|
412
|
+
|
|
413
|
+
The same command also checks that each note's `sohl:` block is what its **type**
|
|
414
|
+
allows (#19). Five classes, all of them mistakes that were previously reported
|
|
415
|
+
somewhere other than where they were made, or not at all:
|
|
416
|
+
|
|
417
|
+
- **Unknown or retired type** — a note on a retired spelling is told what
|
|
418
|
+
replaced it.
|
|
419
|
+
- **Missing required property** — `dimensions` on a map, `subType` on a skill.
|
|
420
|
+
- **Wrong value shape** — `weight: heavy` where a number belongs.
|
|
421
|
+
- **Unknown property** — _the allow-list made loud_. The builders discard a
|
|
422
|
+
`sohl:` key no field declares, with no warning and no effect on the exit code,
|
|
423
|
+
which is how 204 kethira mystical abilities shipped with no affiliation (#3).
|
|
424
|
+
A near miss is named: `Did you mean "masteryLevelBase"?`
|
|
425
|
+
- **Dead shortcode reference** — `assocSkillCode` naming a skill nothing
|
|
426
|
+
declares. Resolved through the same resolver `links` uses, so a cross-package
|
|
427
|
+
reference answered by a vendored manifest lands exactly as it would in a
|
|
428
|
+
wikilink. `--no-references` turns this one off for a tree whose cross-package
|
|
429
|
+
references it cannot see.
|
|
430
|
+
|
|
431
|
+
**A schema says what a note may _write_, not what the compiler emits.** Those
|
|
432
|
+
are different, and the difference is the whole calibration of the check: a note
|
|
433
|
+
also feeds a knowledgebase and a website, and those read classification the pack
|
|
434
|
+
build never compiles — `kbcat` alone appears 51 times in SoHL's knowledgebase
|
|
435
|
+
layouts. Equating the vocabulary with the builder's allow-list reported 4,241
|
|
436
|
+
unknown properties against SoHL's own tree, every one correctly authored.
|
|
437
|
+
|
|
438
|
+
Item types need no separate declaration: their field list already _is_ the
|
|
439
|
+
builder, so schema and compiler cannot disagree. The hand-written compilers —
|
|
440
|
+
`being`, `macro`, `doc` and the three map types — declare theirs in
|
|
441
|
+
`sohl/note-schemas.mjs`.
|
|
442
|
+
|
|
443
|
+
Nothing here writes. A check reports and an author fixes.
|
|
444
|
+
|
|
445
|
+
**A third rule was retired (#79).** Every note used to be required to repeat its
|
|
446
|
+
own `type-shortcode` address in `aliases:`. That served one reader — Obsidian,
|
|
447
|
+
so `[[type-shortcode]]` resolved in the editor — and no build ever read it: both
|
|
448
|
+
resolvers parse the hyphen qualifier themselves. The project no longer authors
|
|
449
|
+
in Obsidian, so the rule cost a line of frontmatter per note for a reader that
|
|
450
|
+
does not exist. Removing it was verified output-neutral first: across 1,735
|
|
451
|
+
stripped notes, `package compile` produced byte-identical `build/packs-json` and
|
|
452
|
+
the site build byte-identical `site/content`.
|
|
453
|
+
|
|
454
|
+
## Prose: formatting and markdown
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
npx content-build format # check the whole repository
|
|
458
|
+
npx content-build format --write # rewrite what is not formatted
|
|
459
|
+
npx content-build markdown # lint every markdown file
|
|
460
|
+
npx content-build markdown --fix # apply the fixes markdownlint can make
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Two conventions every content repository writes to, declared once here so a note
|
|
464
|
+
formatted in one is formatted the same way in the next (#69):
|
|
465
|
+
|
|
466
|
+
- **`format`** runs Prettier. Same values SoHL has always used, so a module or a
|
|
467
|
+
note moving between repositories does not reformat on arrival.
|
|
468
|
+
- **`markdown`** runs markdownlint — the structural checks Prettier cannot make:
|
|
469
|
+
a heading level that skips, two sibling headings claiming one anchor, a
|
|
470
|
+
reversed `(text)[url]`, a bare URL, an empty link, a table row with the wrong
|
|
471
|
+
cell count, and the emphasis markers (`_emphasis_`, `**strong**`) these
|
|
472
|
+
repositories write.
|
|
473
|
+
|
|
474
|
+
The rule set is **deliberately narrow**. markdownlint's defaults over a content
|
|
475
|
+
tree produce tens of thousands of findings, almost all of them line length, list
|
|
476
|
+
indentation and blank lines — Prettier's territory. So `default` is off and each
|
|
477
|
+
rule is enabled by name, with the reason it earns its place; add one only if it
|
|
478
|
+
can report that a page is _wrong_.
|
|
479
|
+
|
|
480
|
+
Both run over the **repository**, not the content tree, and neither reads the
|
|
481
|
+
pack configuration — a repository's formatting covers everything it holds, and
|
|
482
|
+
one that has not configured this package at all can still format itself.
|
|
483
|
+
|
|
484
|
+
**What ships here is a default, not an override.** A consumer's own Prettier
|
|
485
|
+
config or `.markdownlint-cli2.jsonc` wins wherever it has one. Which paths to
|
|
486
|
+
skip is knowledge about a repository's layout and stays with that repository, in
|
|
487
|
+
its own `.prettierignore` and `.gitignore` — both honoured, as Prettier and
|
|
488
|
+
markdownlint honour them natively. The one exception is `CHANGELOG.md`, which
|
|
489
|
+
`changeset version` regenerates in every repository here: linting it reports on
|
|
490
|
+
the generator, so it is skipped by default.
|
|
491
|
+
|
|
492
|
+
Neither tool's file discovery is reimplemented, so `content-build format --check`
|
|
493
|
+
and a bare `prettier --check .` report the same thing. A file Prettier cannot
|
|
494
|
+
parse is a **finding**, with its position — not a crash that costs the report on
|
|
495
|
+
every other file.
|
|
496
|
+
|
|
497
|
+
To make an editor agree with the lint chain, point its config at the same rules:
|
|
498
|
+
|
|
499
|
+
```js
|
|
500
|
+
// prettier.config.mjs
|
|
501
|
+
export { default } from "@heroiclands/package-build/prettier";
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
// .markdownlint-cli2.mjs — extending rather than replacing
|
|
506
|
+
import shared from "@heroiclands/package-build/markdownlint";
|
|
507
|
+
export default { ...shared, config: { ...shared.config, MD013: true } };
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Publishing a link manifest
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
npx content-build manifest # the configured tree and output directory
|
|
514
|
+
npx content-build manifest --out tmp/ # or somewhere else
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Writes `<contentPackage>.json` naming every note this package publishes, keyed by
|
|
518
|
+
the canonical `package-type-shortcode` address and valued with every address that
|
|
519
|
+
note has: a `path` on the web, a `uuid` in Foundry, the `anchors` its named
|
|
520
|
+
sections compiled to, and a `doc` pointer where an item's prose compiles into a
|
|
521
|
+
JournalEntry of its own. A consuming build vendors the file into its own
|
|
522
|
+
`paths.manifests` and resolves cross-package links through it — the counterpart
|
|
523
|
+
of `links`, which consumes what this emits.
|
|
524
|
+
|
|
525
|
+
It reads its whole input from configuration and takes nothing else:
|
|
526
|
+
|
|
527
|
+
| Setting | What it decides |
|
|
528
|
+
| --------------------------- | ------------------------------------------------------------- |
|
|
529
|
+
| `contentPackage` | The package emitted, and which notes belong to it. |
|
|
530
|
+
| `foundryPackage` | The package every emitted `uuid` names. |
|
|
531
|
+
| `paths.content` | The tree walked. |
|
|
532
|
+
| `paths.manifestOut` | Where the file lands (`build/manifests` by default). |
|
|
533
|
+
| `publish.manifests.publish` | Whether this repository publishes one at all. |
|
|
534
|
+
| `publish.site` | Whether entries carry a `path` — see below. |
|
|
535
|
+
| `publish.address` | The address scheme those paths are derived under — see below. |
|
|
536
|
+
|
|
537
|
+
**Both addresses are optional, independently.** A note that compiles into no
|
|
538
|
+
document has no `uuid`, and a package that ships compendiums and publishes no
|
|
539
|
+
site (`publish.site: false`) has no `path` on any entry. Neither is an error, and
|
|
540
|
+
neither is guessed: inventing the missing one asserts a target that does not
|
|
541
|
+
exist, which is the silent dead link the manifest exists to prevent.
|
|
542
|
+
|
|
543
|
+
**`publish.manifests.publish` is a declaration, not a preference.** The file is
|
|
544
|
+
vendored by other repositories and read as authoritative, so emitting one is a
|
|
545
|
+
statement about this package. With the switch off the command fails rather than
|
|
546
|
+
writing.
|
|
547
|
+
|
|
548
|
+
### The address scheme
|
|
549
|
+
|
|
550
|
+
Where the content tree mounts _inside the package_, and which note addresses a
|
|
551
|
+
whole section rather than a page within one, differ between repositories and are
|
|
552
|
+
both load-bearing. They are one setting, read by this command **and** by the page
|
|
553
|
+
emitter, so the address a manifest publishes is the address a page is emitted at
|
|
554
|
+
— stating it twice is how a manifest comes to assert a URL that resolves at build
|
|
555
|
+
time and 404s for the reader.
|
|
556
|
+
|
|
557
|
+
```yaml
|
|
558
|
+
publish:
|
|
559
|
+
site: true
|
|
560
|
+
manifests: { publish: true, consume: true }
|
|
561
|
+
address:
|
|
562
|
+
prefix: kb/ # default: "" — the package root
|
|
563
|
+
landing: readme # default: readme
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
- **`prefix`** — the content tree's mount within the package. `sohl` publishes a
|
|
567
|
+
knowledgebase alongside generated API docs, so its notes sit under `kb/`
|
|
568
|
+
(`kb/affliction/aconite/`); `thalorna`'s site is nothing but its content, so it
|
|
569
|
+
has no prefix (`affiliation/the-aerarium-imperii/`). It must end in a slash and
|
|
570
|
+
must not begin with one — where the _package_ is mounted is the consuming
|
|
571
|
+
build's knowledge and is never recorded here.
|
|
572
|
+
- **`landing`** — which note is a section's landing page, and so has no slug of
|
|
573
|
+
its own:
|
|
574
|
+
- `readme` — a `README.md` addresses its section. A `doc` note then routes by
|
|
575
|
+
its `category` like any other, so a `category: collection` note publishes
|
|
576
|
+
under a literal `collection/` section.
|
|
577
|
+
- `collection` — a `doc` note whose `category` is `collection` addresses the
|
|
578
|
+
section it introduces, named by its authored `section`.
|
|
579
|
+
|
|
580
|
+
The two are alternatives rather than a pair that could both apply: each live
|
|
581
|
+
content tree holds notes the other rule would move.
|
|
582
|
+
|
|
583
|
+
A note the scheme yields no address for — a `doc` with no category, a collection
|
|
584
|
+
note naming no section — is **reported and omitted**, never guessed. The command
|
|
585
|
+
prints one located diagnostic per note and still writes the file, because a note
|
|
586
|
+
with no address is ordinary while a manifest entry pointing at a page that does
|
|
587
|
+
not exist is not.
|
|
588
|
+
|
|
589
|
+
## Publishing a website
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
npx content-build site # the configured tree and output
|
|
593
|
+
npx content-build site --out tmp/kb # or somewhere else
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
The sibling of `package compile`: the same content tree, rendered as pages
|
|
597
|
+
instead of compiled into packs. It does the walk, the frontmatter read, the
|
|
598
|
+
address derivation, the address index, table expansion, wikilink resolution,
|
|
599
|
+
code-fence protection, the foreign-manifest merge, the page emission and the
|
|
600
|
+
section-landing backfill.
|
|
601
|
+
|
|
602
|
+
**What it does not do is decide addresses.** Those come from `publish.address`,
|
|
603
|
+
the same setting the link manifest reads, so a page and its manifest entry cannot
|
|
604
|
+
disagree about where the page is. Everything under `site:` is _framing_ —
|
|
605
|
+
where the tree is written, what a section is called, which extra trees are
|
|
606
|
+
published beside the content:
|
|
607
|
+
|
|
608
|
+
```yaml
|
|
609
|
+
site:
|
|
610
|
+
out: kb/content # required; wiped on every run
|
|
611
|
+
base: /sohl/ # default: /<contentPackage>/
|
|
612
|
+
packages: [sohl, thalorna] # default: just contentPackage
|
|
613
|
+
backfillSections: true
|
|
614
|
+
landing: { title: Knowledgebase, type: knowledgebase }
|
|
615
|
+
pass: sohlKb
|
|
616
|
+
passOptions:
|
|
617
|
+
apiBase: /sohl/api/
|
|
618
|
+
symbolMap: kb/data/api-symbols.json
|
|
619
|
+
blob: https://github.com/HeroicLands/…/blob/main/
|
|
620
|
+
trees:
|
|
621
|
+
- { from: kb/dev-docs, section: dev-docs }
|
|
622
|
+
sections:
|
|
623
|
+
being: { title: Beings, banner: banners/creature.webp }
|
|
624
|
+
readmeSections:
|
|
625
|
+
dev-docs: { title: Developer Documentation, banner: banners/dev-docs.webp }
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
| Key | What it decides |
|
|
629
|
+
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
|
630
|
+
| `out` | The Hugo content root. **Required**, and wiped on every run — see the safety note below. |
|
|
631
|
+
| `base` | Where the package is served. Defaults to `/<contentPackage>/`. |
|
|
632
|
+
| `packages` | Which content packages this site renders. Defaults to its own. |
|
|
633
|
+
| `sections` | Landing title and hero per section, so a landing matches the card that links to it. |
|
|
634
|
+
| `readmeSections` | The same, for a section whose landing comes from a `README`. |
|
|
635
|
+
| `landing` | Frontmatter for the mount's own `_index.md`. Passed through — the vocabulary is the theme's. |
|
|
636
|
+
| `backfillSections` | Write a bare `_index.md` for any other section directly under the mount. |
|
|
637
|
+
| `trees` | Extra source trees published beside the content, preserving their source layout below a section. |
|
|
638
|
+
| `pass` | A named bundle of this repository's own body rewrites. |
|
|
639
|
+
| `passOptions` | That bundle's options. |
|
|
640
|
+
|
|
641
|
+
### Why `out` is required
|
|
642
|
+
|
|
643
|
+
The output tree is a build artifact and is **deleted on every run**, so that a
|
|
644
|
+
page whose note was renamed cannot linger and keep publishing. An unset `out`
|
|
645
|
+
resolves to the repository root, and the wipe then deletes the working tree.
|
|
646
|
+
That is not hypothetical — it happened while this command was being written, on
|
|
647
|
+
a configuration that simply had no `site` section yet. So `out` is refused when
|
|
648
|
+
unset, and refused again when it resolves anywhere that is not strictly inside
|
|
649
|
+
the repository root.
|
|
650
|
+
|
|
651
|
+
### Consumer passes are named, not imported
|
|
652
|
+
|
|
653
|
+
A repository's own body rewrites are code, and a configuration is data, so a
|
|
654
|
+
configuration **names** a bundle and the toolchain resolves it — exactly as
|
|
655
|
+
`itemBuilders` names an item registry. `sohlKb` is the bundle for the `sohl`
|
|
656
|
+
knowledgebase: it resolves `{@link}` tags against a TypeDoc symbol map and
|
|
657
|
+
rewrites repository-relative links in the developer docs to their published or
|
|
658
|
+
GitHub addresses. Neither rewrite can fail a build; an unknown `{@link}` degrades
|
|
659
|
+
to a code span.
|
|
660
|
+
|
|
661
|
+
A bundle supplies up to two hooks, and their order around the shared work is the
|
|
662
|
+
point:
|
|
663
|
+
|
|
664
|
+
1. `beforeLinks`, on every page, before wikilinks resolve — a `{@link}` tag may
|
|
665
|
+
sit in prose a wikilink also touches.
|
|
666
|
+
2. `afterLinks`, on pages from an extra tree only — repository-relative links are
|
|
667
|
+
a property of how those pages are authored, not of content notes.
|
|
668
|
+
|
|
669
|
+
Both run inside code-fence protection, so neither can rewrite a fenced example.
|
|
670
|
+
|
|
671
|
+
### The gates
|
|
672
|
+
|
|
673
|
+
Every integrity check reports and the run stops at the first that fires, so the
|
|
674
|
+
output names the cause rather than its symptoms — an unusable manifest reported
|
|
675
|
+
after the links that failed because of it reads as a pile of broken notes.
|
|
676
|
+
|
|
677
|
+
| Gate | What it catches |
|
|
678
|
+
| ---------------------- | ---------------------------------------------------------------------------------- |
|
|
679
|
+
| Frontmatter wikilinks | A link in frontmatter, which is copied verbatim and reaches the reader as `[[…]]`. |
|
|
680
|
+
| Slugs | A name that yields no URL. |
|
|
681
|
+
| Collisions | Two notes claiming one page URL. |
|
|
682
|
+
| Unusable manifest | A vendored manifest this build cannot read. |
|
|
683
|
+
| Unaddressable manifest | One it can read but cannot look anything up in. |
|
|
684
|
+
| Package conflicts | One address claimed by two packages. |
|
|
685
|
+
| Tables and wikilinks | A table directive that cannot be honoured, or a link that lands nowhere. |
|
|
686
|
+
|
|
687
|
+
None of them exits the process from inside the library; the command decides. That
|
|
688
|
+
is what makes them testable, which the consumer scripts' inline `process.exit`
|
|
689
|
+
calls were not.
|
|
690
|
+
|
|
691
|
+
## Diagnostics
|
|
692
|
+
|
|
693
|
+
Every warning or error a build reports **about a content note** is emitted in the
|
|
694
|
+
form every C-family compiler, `tsc` and ESLint already use, so an editor, a CI
|
|
695
|
+
annotator or a `grep` parses it with no knowledge of this build:
|
|
696
|
+
|
|
697
|
+
```text
|
|
698
|
+
assets/content/Regions/Capital_Nome.md:43:635: warning: unresolved wikilink [[Kenbet_Pat|Kenbet'Pat]] (unknown) in "The Capital Nome"
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
`file:line:column: severity: message`. The path is relative to the working
|
|
702
|
+
directory — during a build, the consuming repository's root.
|
|
703
|
+
|
|
704
|
+
Two rules keep it that way, both in `engine/diagnostics.mjs`:
|
|
705
|
+
|
|
706
|
+
- **The locator starts the line.** Diagnostics deliberately bypass `loglevel`,
|
|
707
|
+
whose `[timestamp] [WARN]:` prefix sits exactly where a parser reads the path
|
|
708
|
+
from; a greedy path pattern swallows the prefix and yields a filename nothing
|
|
709
|
+
can open. Progress and summary lines still go through `loglevel` — they are
|
|
710
|
+
not about a file and nothing needs to parse them.
|
|
711
|
+
- **A field is dropped, never guessed.** A diagnostic reports the position it
|
|
712
|
+
can establish honestly and no more: `file:line: …` when the column is
|
|
713
|
+
meaningless, `file: …` when only the note is known. Nothing defaults to
|
|
714
|
+
`1:1`, which would send a reader to the frontmatter every time.
|
|
715
|
+
|
|
716
|
+
Establishing a position at all takes three corrections, applied only where they
|
|
717
|
+
hold — see `positionInBody`. A body offset is not a file line until the
|
|
718
|
+
frontmatter's lines are added (`bodyLine`); the trim that strips the body can
|
|
719
|
+
take indentation off its first line (`bodyColumn`); and a body is scanned
|
|
720
|
+
_after_ its content tables expand, so an offset may land in text nobody
|
|
721
|
+
authored. `expandContentTables` therefore returns a `lineMap` saying which
|
|
722
|
+
authored line each emitted line came from — a generated row is blamed on the
|
|
723
|
+
directive that produced it and reports **no column**, because there is no
|
|
724
|
+
authored character to point at.
|
|
725
|
+
|
|
726
|
+
## Layout
|
|
727
|
+
|
|
728
|
+
- **`@heroiclands/package-build/engine`** — package-agnostic machinery: the
|
|
729
|
+
content walk, frontmatter, tables, wikilinks, ids, folders, the link manifest
|
|
730
|
+
and the web-address rule, `BasePackCompiler`, and the generic Foundry document
|
|
731
|
+
compilers.
|
|
732
|
+
- **`@heroiclands/package-build/sohl`** — Song of Heroic Lands data-model
|
|
733
|
+
knowledge: item types, builders, the items and actors compilers, and default
|
|
734
|
+
art. Isolated behind its own entry point so an adventure module never receives
|
|
735
|
+
`buildWeaponGear`.
|
|
736
|
+
|
|
737
|
+
Each module is also reachable as its own entry point —
|
|
738
|
+
`@heroiclands/package-build/engine/journals`,
|
|
739
|
+
`@heroiclands/package-build/sohl/items` — so a build that needs one thing does
|
|
740
|
+
not load the whole pipeline. The barrels re-export each module as a namespace
|
|
741
|
+
rather than flattening it, because several modules deliberately re-export a
|
|
742
|
+
neighbour's symbol and a flattened star export would drop every such name
|
|
743
|
+
silently.
|
|
744
|
+
|
|
745
|
+
A few plain-ESM leaves are shared **with the Foundry runtime**, not just with the
|
|
746
|
+
build: the item default-art map, the curated region-event vocabulary, and the
|
|
747
|
+
affiliation standings. Each has its own entry point —
|
|
748
|
+
`@heroiclands/package-build/sohl/default-item-art`,
|
|
749
|
+
`.../engine/region-events`, `.../sohl/affiliation-standings` — so a client bundle
|
|
750
|
+
reaches the constant without importing a barrel that grows to hold compilers
|
|
751
|
+
reading the filesystem. Keeping one copy of each is the point: the build-time and
|
|
752
|
+
runtime values cannot disagree, which is the drift that produced #932.
|
|
753
|
+
|
|
754
|
+
`@heroiclands/package-build/content-config` exposes the configuration contract's own
|
|
755
|
+
module, so a consumer can name its types (`ContentBuildConfig`, `PackSpec`) from
|
|
756
|
+
JSDoc.
|
|
757
|
+
|
|
758
|
+
## Tests
|
|
759
|
+
|
|
760
|
+
The package carries its own suite and its own vitest project, so it is
|
|
761
|
+
verifiable without the repository that happens to host it:
|
|
762
|
+
|
|
763
|
+
```
|
|
764
|
+
npm test -w @heroiclands/package-build # from the SoHL repository root
|
|
765
|
+
npm test # from packages/content-build/
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
The SoHL repository's root `npm run test` names the very same project config, so
|
|
769
|
+
one command still gates everything CI runs and neither entry point can drift
|
|
770
|
+
into a different suite.
|
|
771
|
+
|
|
772
|
+
The harness is deliberately austere: no global setup, no Foundry stubs, and no
|
|
773
|
+
alias onto a consuming repository's source. `tests/suite-is-self-contained.test.ts`
|
|
774
|
+
enforces that — a test in this suite that reached for `globalThis.game` or `@src`
|
|
775
|
+
would pass in situ and fail the moment the package was installed from npm.
|
|
776
|
+
|
|
777
|
+
`tests/dependencies-are-declared.test.ts` guards the same failure from the
|
|
778
|
+
shipping side. Because this package is a workspace, npm hoists the root
|
|
779
|
+
repository's `devDependencies` into the workspace root, so an import this
|
|
780
|
+
package never declared still resolves here and fails nowhere but a consumer's
|
|
781
|
+
install (#1557). The test walks every module named by the `files` field and
|
|
782
|
+
holds each bare specifier to one of three cases — a Node builtin, this package
|
|
783
|
+
addressing itself, or a declared `dependency` — and checks the converse: nothing
|
|
784
|
+
shipped may import a `devDependency`, and no declared dependency may go
|
|
785
|
+
unimported.
|
|
786
|
+
|
|
787
|
+
## Releasing
|
|
788
|
+
|
|
789
|
+
Releasing is not a command anyone runs. It is a consequence of merging, in two
|
|
790
|
+
steps, and each step is visible while it is pending.
|
|
791
|
+
|
|
792
|
+
**Every pull request declares its bump.** Run `npx changeset` and pick
|
|
793
|
+
major/minor/patch; the summary you write becomes the changelog entry and the
|
|
794
|
+
release note. If the change ships nothing a consumer can see, say so explicitly
|
|
795
|
+
with `npx changeset add --empty`. CI's **Changeset declared** job fails a pull
|
|
796
|
+
request that declares neither — `npm run changeset:check` is the same check,
|
|
797
|
+
locally.
|
|
798
|
+
|
|
799
|
+
**Merging to `main` opens a Version Packages pull request** carrying the version
|
|
800
|
+
bump and the rewritten `CHANGELOG.md`. That pull request _is_ the pending
|
|
801
|
+
release: as long as something is merged but unpublished, there is an open pull
|
|
802
|
+
request saying so. This is the whole point of the pipeline — the previous,
|
|
803
|
+
hand-driven process failed by leaving _nothing_ behind when the final step was
|
|
804
|
+
forgotten, and on 2026-08-21 it did exactly that for two versions (#15).
|
|
805
|
+
|
|
806
|
+
**Merging that publishes.** `changeset publish` puts the version on npm through
|
|
807
|
+
Trusted Publishing (OIDC — there is no `NPM_TOKEN`), tags the commit `v<version>`
|
|
808
|
+
and cuts the GitHub Release with the changelog section as its body. It publishes
|
|
809
|
+
only versions that are not already on the registry, so re-running it is a no-op;
|
|
810
|
+
`workflow_dispatch` on **Publish to npm** is the recovery path if a run fails
|
|
811
|
+
after versioning.
|
|
812
|
+
|
|
813
|
+
Below 1.0.0, `^0.x` never crosses a minor — a consumer on `^0.15.0` will not see
|
|
814
|
+
`0.16.0` until it bumps the pin deliberately. Dependabot raises that as its own
|
|
815
|
+
pull request in each of the three consuming repositories.
|
|
816
|
+
|
|
817
|
+
> After a successful publish, `npm view @heroiclands/package-build version` can
|
|
818
|
+
> report the _previous_ version for a minute or so. `dist-tags` is correct
|
|
819
|
+
> immediately, and is what the workflow prints.
|
|
820
|
+
|
|
821
|
+
## License
|
|
822
|
+
|
|
823
|
+
GPL-3.0-or-later — see the
|
|
824
|
+
[SoHL repository](https://github.com/HeroicLands/Song-of-Heroic-Lands-FoundryVTT).
|