@heroiclands/package-build 20.4.0 → 20.6.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 +288 -0
- package/CONTENT.md +213 -20
- package/README.md +19 -1
- package/bin/content-build.mjs +135 -32
- package/bin/package-build.mjs +46 -13
- package/content-config.mjs +345 -101
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +16 -6
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/engine/actor-compiler.mjs +30 -27
- package/engine/address-diff.mjs +45 -41
- package/engine/base-compiler.mjs +6 -0
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +9 -9
- package/engine/content-index.mjs +44 -23
- package/engine/content-links.mjs +44 -11
- package/engine/content-lint.mjs +44 -10
- package/engine/content-tables.mjs +32 -27
- package/engine/folder-notes.mjs +4 -2
- package/engine/frontmatter-lint.mjs +35 -38
- package/engine/generate.mjs +5 -0
- package/engine/helpers.mjs +86 -32
- package/engine/index.mjs +12 -2
- package/engine/journals.mjs +9 -0
- package/engine/note-claims.mjs +18 -10
- package/engine/note-schemas.mjs +0 -5
- package/engine/note-vocabulary.mjs +32 -31
- package/engine/pack-config.mjs +26 -12
- package/engine/pack-router.mjs +0 -0
- package/engine/pdf-build.mjs +464 -0
- package/engine/pdf-fonts.mjs +420 -0
- package/engine/pdf-render.mjs +876 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/scenes.mjs +14 -5
- package/engine/schema-check.mjs +1 -1
- package/engine/site-build.mjs +21 -3
- package/engine/web-wikilinks.mjs +6 -3
- package/engine/wikilinks.mjs +2 -4
- package/hm3/actors.mjs +8 -0
- package/hm3/items.mjs +8 -0
- package/package.json +1 -1
- package/release.mjs +63 -3
- package/sohl/actors.mjs +8 -0
- package/sohl/items.mjs +8 -0
- package/sohl/note-schemas.mjs +5 -5
- package/types/content-config.d.mts +66 -15
- package/types/engine/actor-compiler.d.mts +34 -30
- package/types/engine/address-diff.d.mts +57 -3
- package/types/engine/base-compiler.d.mts +10 -2
- package/types/engine/bundles.d.mts +9 -0
- package/types/engine/content-address.d.mts +9 -9
- package/types/engine/content-index.d.mts +57 -13
- package/types/engine/content-lint.d.mts +6 -4
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/frontmatter-lint.d.mts +3 -2
- package/types/engine/helpers.d.mts +105 -31
- package/types/engine/index.d.mts +4 -0
- package/types/engine/journals.d.mts +9 -0
- package/types/engine/note-claims.d.mts +17 -10
- package/types/engine/note-vocabulary.d.mts +23 -196
- package/types/engine/pack-config.d.mts +4 -4
- package/types/engine/pdf-build.d.mts +42 -0
- package/types/engine/pdf-fonts.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +156 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +2 -2
- package/types/engine/site-build.d.mts +34 -6
- package/types/engine/wikilinks.d.mts +2 -3
- package/types/hm3/actors.d.mts +8 -0
- package/types/hm3/items.d.mts +8 -0
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +10 -2
- package/types/sohl/items.d.mts +8 -0
package/docs/api.md
ADDED
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
`@heroiclands/package-build` exposes its programmatic surface through
|
|
4
|
+
subpath entries, declared in `package.json`'s `exports` map. Every function
|
|
5
|
+
and value documented here is a supported contract, not an internal seam —
|
|
6
|
+
`./sohl` and `./hm3` included.
|
|
7
|
+
|
|
8
|
+
**Everything exported anywhere in this package is pure.** A function takes
|
|
9
|
+
source text or already-loaded data and returns findings or values;
|
|
10
|
+
discovery, I/O and reporting stay with the caller. That is what lets one
|
|
11
|
+
rule set serve a lint script, a build step and a unit test without any of
|
|
12
|
+
them having to agree on how files are found or how findings are printed. A
|
|
13
|
+
handful of packaging functions — deploying, staging, running a container,
|
|
14
|
+
packing a release — necessarily touch the filesystem or a subprocess; each
|
|
15
|
+
is named for it in the tables below, and the pure half of its module is kept
|
|
16
|
+
separate from it.
|
|
17
|
+
|
|
18
|
+
This reference is organized by subpath entry, matching how a consumer
|
|
19
|
+
imports: `import * as engine from "@heroiclands/package-build/engine"` reads
|
|
20
|
+
the engine namespaces directly, while `import { engine } from
|
|
21
|
+
"@heroiclands/package-build"` reaches the same namespaces one level deeper,
|
|
22
|
+
as a property of the root import. Each export's row states its signature,
|
|
23
|
+
what it returns, and when a caller reaches for it — sourced from the
|
|
24
|
+
export's own JSDoc.
|
|
25
|
+
|
|
26
|
+
## `.` — the package root
|
|
27
|
+
|
|
28
|
+
`@heroiclands/package-build` — the shared toolchain for building and shipping a HeroicLands **Foundry package**, content and all. Importing the bare package name re-exports the content configuration contract plus every namespace documented below, so a consumer that wants the whole surface under one import can use this entry instead of reaching for each subpath individually.
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { defineConfig, engine, sohl } from "@heroiclands/package-build";
|
|
32
|
+
|
|
33
|
+
const config = defineConfig({
|
|
34
|
+
rootDir: import.meta.dirname,
|
|
35
|
+
contentPackage: "example",
|
|
36
|
+
foundryPackage: "example",
|
|
37
|
+
packageKind: "modules",
|
|
38
|
+
stats: { lastModifiedBy: "examplebuilder00" },
|
|
39
|
+
packs: [{ name: "items", type: "Item" }],
|
|
40
|
+
compatibility: { minimum: "14.359" },
|
|
41
|
+
});
|
|
42
|
+
const { addressSlug } = engine.contentAddress;
|
|
43
|
+
console.log(addressSlug({ type: "weapongear", shortcode: "dagger" })); // "weapongear-dagger"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Export | What it is |
|
|
47
|
+
| --------------------- | --------------------------------- |
|
|
48
|
+
| `defineConfig` | Function — see `./content-config` |
|
|
49
|
+
| `PACKAGE_KINDS` | Const — see `./content-config` |
|
|
50
|
+
| `PACK_DOCUMENT_TYPES` | Const — see `./content-config` |
|
|
51
|
+
| `engine` | Namespace — see `./engine` |
|
|
52
|
+
| `sohl` | Namespace — see `./sohl` |
|
|
53
|
+
| `manifest` | Namespace — see `./manifest` |
|
|
54
|
+
| `bundle` | Namespace — see `./bundle` |
|
|
55
|
+
| `stage` | Namespace — see `./stage` |
|
|
56
|
+
| `release` | Namespace — see `./release` |
|
|
57
|
+
| `deploy` | Namespace — see `./deploy` |
|
|
58
|
+
| `container` | Namespace — see `./container` |
|
|
59
|
+
| `e2e` | Namespace — see `./e2e` |
|
|
60
|
+
| `lang` | Namespace — see `./lang` |
|
|
61
|
+
| `coverage` | Namespace — see `./coverage` |
|
|
62
|
+
| `templates` | Namespace — see `./templates` |
|
|
63
|
+
|
|
64
|
+
`hm3` is **not** re-exported from the root — import `@heroiclands/package-build/hm3` directly.
|
|
65
|
+
|
|
66
|
+
## `./engine`
|
|
67
|
+
|
|
68
|
+
The package-agnostic half of the toolchain: everything that knows how a
|
|
69
|
+
HeroicLands content tree is shaped, but nothing about any particular game
|
|
70
|
+
system's data model. The content walk, frontmatter parsing, table
|
|
71
|
+
generation, wikilink resolution, id and folder derivation, the link
|
|
72
|
+
manifest and the web-address rule, `BasePackCompiler`, and the generic
|
|
73
|
+
Foundry document compilers (journals, macros, scenes) all live here.
|
|
74
|
+
|
|
75
|
+
**Each module is re-exported as its own namespace, not flattened.** Several
|
|
76
|
+
namespaces deliberately re-export a neighbour's symbol so a caller keeps one
|
|
77
|
+
import path — `helpers` re-exports the frontmatter readers and `makeId`;
|
|
78
|
+
`wikilinks` re-exports the pack router. Flattened, every such name would
|
|
79
|
+
become an ambiguous star export and vanish from this barrel silently.
|
|
80
|
+
|
|
81
|
+
**Every module is also reachable as its own entry point** —
|
|
82
|
+
`@heroiclands/package-build/engine/<module>` (e.g.
|
|
83
|
+
`@heroiclands/package-build/engine/content-address`) — which is how a build
|
|
84
|
+
that needs one thing avoids loading the whole pipeline.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import { engine } from "@heroiclands/package-build";
|
|
88
|
+
// or, to load only what is needed:
|
|
89
|
+
import * as contentAddress from "@heroiclands/package-build/engine/content-address";
|
|
90
|
+
|
|
91
|
+
const { addressSlug } = engine.contentAddress;
|
|
92
|
+
console.log(addressSlug({ type: "weapongear", shortcode: "dagger" }));
|
|
93
|
+
// -> "weapongear-dagger"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `engine.ids`
|
|
97
|
+
|
|
98
|
+
Deterministic document ids, derived by hashing rather than stored, so compile passes that cannot see each other's output still agree on an id. Also holds the pack-name vocabulary — which content type compiles into which conventional pack and document type — and the retired/renamed type tables every type-keyed lookup normalizes through first.
|
|
99
|
+
|
|
100
|
+
| Export | Signature | Returns | Use it when |
|
|
101
|
+
| ---------------------- | ----------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
102
|
+
| `makeId` | `makeId(namespace, value)` | `string` — a 16-character hexadecimal Foundry id | deriving a stable id from a namespace and a value, e.g. a heading id when none is supplied |
|
|
103
|
+
| `MAP_TYPES` | `const MAP_TYPES` | — | enumerating the content types that compile into a Foundry `Scene` |
|
|
104
|
+
| `MAP_SUBTYPES` | `const MAP_SUBTYPES` | — | reading the map subTypes, which differ only in derived canvas defaults |
|
|
105
|
+
| `JOURNAL_TYPES` | `const JOURNAL_TYPES` | — | enumerating content types whose whole document _is_ a JournalEntry |
|
|
106
|
+
| `PACK_BY_TYPE` | `const PACK_BY_TYPE` | — | looking up the conventional pack name and document type for a content type |
|
|
107
|
+
| `RETIRED_TYPES` | `const RETIRED_TYPES` | — | looking up what a retired content type was replaced by |
|
|
108
|
+
| `assertTypeNotRetired` | `assertTypeNotRetired(type, where)` | throws | refusing a note whose type has been retired outright |
|
|
109
|
+
| `RENAMED_TYPES` | `const RENAMED_TYPES` | — | looking up what a renamed content type is now called |
|
|
110
|
+
| `currentType` | `currentType(type)` | the current spelling | normalizing a type to its current spelling before any keyed lookup |
|
|
111
|
+
| `renamedTypeMessage` | `renamedTypeMessage(retired, current, where)` | `string` — the finding message | building the one message every reporter of a renamed type shares |
|
|
112
|
+
| `ITEM_PACK` | `const ITEM_PACK` | — | naming the pack every open-set item type compiles into by default |
|
|
113
|
+
| `packForType` | `packForType(type)` | `{pack, docType}` | resolving the pack and document type an item type's documents live in |
|
|
114
|
+
| `compendiumUuid` | `compendiumUuid(packageId, type, id, packName)` | `string` — `Compendium.<packageId>.<pack>.<DocumentType>.<id>` | composing a document's full compendium UUID in the one place it is spelled |
|
|
115
|
+
| `pageUuid` | `pageUuid(entryUuid, pageId)` | `string` — the page's UUID | composing the UUID of a JournalEntry page |
|
|
116
|
+
|
|
117
|
+
### `engine.systemBlock`
|
|
118
|
+
|
|
119
|
+
The per-system frontmatter block: how one note feeds more than one game system through properties named after that system (`<system>.system`, `<system>.type`, `<system>.img`, `<system>.items` on actors). Resolves a field's value through the block, the shared top level, and a retiring position in that order, and merges an authored `<system>.system` onto a compiler-built one without disturbing what the builder already wrote.
|
|
120
|
+
|
|
121
|
+
| Export | Signature | Returns | Use it when |
|
|
122
|
+
| --------------------------- | --------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `SYSTEM_DATA_KEY` | `const SYSTEM_DATA_KEY` | — | naming the key inside a system block whose contents are the system's own vocabulary |
|
|
124
|
+
| `BLOCK_DOCUMENT_PROPERTIES` | `const BLOCK_DOCUMENT_PROPERTIES` | — | mapping a system-block key to the document property it becomes |
|
|
125
|
+
| `BLOCK_DIRECTIVES` | `const BLOCK_DIRECTIVES` | — | recognizing a build directive (e.g. `pack`) that names no document property |
|
|
126
|
+
| `SYSTEM_BLOCK_KEYS` | `const SYSTEM_BLOCK_KEYS` | — | enumerating every key any system block may carry |
|
|
127
|
+
| `setPath` | `setPath(target, dotted, value)` | `object` — `target`, for chaining | writing a value at a dotted path in a document's `system` block |
|
|
128
|
+
| `systemBlock` | `systemBlock(fm, block)` | `Record<string, unknown> \| undefined` | reading one system's whole block off a note, or nothing when it is absent or malformed |
|
|
129
|
+
| `carriesSystemBlock` | `carriesSystemBlock(fm, block)` | `boolean` | deciding whether a pack that declares a system should compile a note at all |
|
|
130
|
+
| `systemData` | `systemData(fm, block)` | `Record<string, unknown>` | reading a system block's `system` sub-block, `{}` when absent |
|
|
131
|
+
| `blockField` | `blockField(fm, block, key, defaultValue)` | the value | reading a key from one system's block, falling back to the top level |
|
|
132
|
+
| `sharedProperty` | `sharedProperty(fm, source, defaultValue)` | the value | reading a shared top-level property by a possibly-dotted path, blind to every system block |
|
|
133
|
+
| `blockProperty` | `blockProperty(fm, block, key, defaultValue)` | the value | reading a property a system block may override, else the shared top-level one |
|
|
134
|
+
| `legacyKeyOf` | `legacyKeyOf(field)` | `string \| undefined` | finding the key a field is authored at inside a system block |
|
|
135
|
+
| `retiredTopLevelKey` | `retiredTopLevelKey(field)` | `string \| undefined` | finding the bare top-level key a `data:`-sourced field is being swept off |
|
|
136
|
+
| `resolveFieldValue` | `resolveFieldValue(field, fm, ...)` | `{value, from}` | resolving one declared field against a note, in the declared source order |
|
|
137
|
+
| `systemDataPaths` | `systemDataPaths(data, prefix)` | `string[]` | listing every path a note authors under `<system>.system`, containers included |
|
|
138
|
+
| `undeclaredPaths` | `undeclaredPaths(data, declared, prefix)` | `string[]` | finding authored paths a system's published schema does not declare |
|
|
139
|
+
| `unknownBlockKeys` | `unknownBlockKeys(fm, block, ...)` | `string[]` | finding keys directly under a system block that neither this format nor the system recognizes |
|
|
140
|
+
| `claimedPaths` | `claimedPaths(fields)` | `Set<string>` | listing the exact `system` paths a field declaration writes, for `mergeSystemData` |
|
|
141
|
+
| `mergeSystemData` | `mergeSystemData(built, fm, ...)` | `object` — `built`, for chaining | merging a note's `<system>.system` verbatim onto a built `system` block |
|
|
142
|
+
|
|
143
|
+
### `engine.codeFences`
|
|
144
|
+
|
|
145
|
+
Where code lives in a markdown body, so a rewriter can leave it alone. Every build-time rewriter that pattern-matches a body needs to know where code is, or a source listing that happens to contain the rewriter's own syntax is silently corrupted.
|
|
146
|
+
|
|
147
|
+
| Export | Signature | Returns | Use it when |
|
|
148
|
+
| --------------------- | ---------------------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
149
|
+
| `FENCE_LINE` | `const FENCE_LINE` | — | matching a fence line, capturing its indent, marker, and info string |
|
|
150
|
+
| `parseHeaderArgs` | `parseHeaderArgs(info)` | `{language, args}` | reading a fence's info string as org-babel header arguments |
|
|
151
|
+
| `codeRegions` | `codeRegions(markdown, ...)` | `Array<{start, end}>` | getting every code region in a markdown body as character offsets |
|
|
152
|
+
| `replaceOutsideCode` | `replaceOutsideCode(markdown, pattern, replacer, options)` | `string` — the rewritten body | running `String.prototype.replace` while skipping anything inside code |
|
|
153
|
+
| `matchAllOutsideCode` | `matchAllOutsideCode(markdown, pattern, options)` | `Array<RegExpMatchArray>` | running `String.prototype.matchAll` while skipping anything inside code |
|
|
154
|
+
| `protectCode` | `protectCode(body, transform)` | `string` — the transformed body | running an arbitrary transform over a body while restoring every code run verbatim afterward |
|
|
155
|
+
|
|
156
|
+
### `engine.frontmatter`
|
|
157
|
+
|
|
158
|
+
Frontmatter readers: pure functions that read a content note's `sohl:` block and normalize what they find. A dependency-free leaf module — its only import is a frozen constants list — so the item-type registry can use these readers without pulling in `helpers.mjs` and the wikilink cycle behind it.
|
|
159
|
+
|
|
160
|
+
| Export | Signature | Returns | Use it when |
|
|
161
|
+
| ----------------------- | --------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------- |
|
|
162
|
+
| `getFrontmatter` | `getFrontmatter(fm, key, defaultValue)` | the value or `defaultValue` | resolving a dotted frontmatter key (e.g. `"name.full"`) into its nested value |
|
|
163
|
+
| `sohlField` | `sohlField(fm, key, defaultValue)` | the value | reading a key from `fm.sohl`, dotted notation supported, falling back to the top level |
|
|
164
|
+
| `sohlSystemField` | `sohlSystemField(fm, to, defaultValue)` | the value | reading a `sohl:` field by also checking its destination position |
|
|
165
|
+
| `resolveCharges` | `resolveCharges(fm)` | `{value, max}` | resolving the `charges` block shared by Mystery and Mystical Ability items |
|
|
166
|
+
| `resolveSkillAptitudes` | `resolveSkillAptitudes(fm, ctx)` | `Record<string, number>` | resolving an item's `skillAptitudes` selector → modifier map |
|
|
167
|
+
| `resolveRelation` | `resolveRelation(fm, ctx)` | `Record<string, string>` | resolving an affiliation's `relation` map of standings toward other affiliations |
|
|
168
|
+
| `requireSubType` | `requireSubType(fm, ctx)` | `string` | reading a mandatory `subType`, throwing when it is absent or blank |
|
|
169
|
+
| `parseValueDesc` | `parseValueDesc(raw)` | a normalized threshold list | parsing the `valueDesc` / threshold array format, either string or object form |
|
|
170
|
+
| `folderField` | `folderField(fm)` | `{value, isAddress}` | reading the compendium folder a note names, as its address |
|
|
171
|
+
|
|
172
|
+
### `engine.contentTree`
|
|
173
|
+
|
|
174
|
+
Whether a content tree has anything to compile. The pack build's worst failure is a success on nothing — an absent or empty tree that compiles zero documents and ships empty compendiums with no error — so this is the empty-tree guard's evidence.
|
|
175
|
+
|
|
176
|
+
| Export | Signature | Returns | Use it when |
|
|
177
|
+
| ------------------- | ------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
178
|
+
| `countContentNotes` | `countContentNotes(root)` | `number` — `.md` note count, `0` when the tree is absent | guarding a build against a content tree that resolves to nothing |
|
|
179
|
+
|
|
180
|
+
### `engine.packConfig`
|
|
181
|
+
|
|
182
|
+
The consuming repository's resolved `package-build.config.yaml`: the content package, the Foundry package and its kind, every path, the `_stats` identity, item-type membership and the pack list — read on call rather than hoisted at import, so importing this package never requires a configuration to exist.
|
|
183
|
+
|
|
184
|
+
| Export | Signature | Returns | Use it when |
|
|
185
|
+
| ------------------- | ------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------ |
|
|
186
|
+
| `CONFIG_BASENAME` | `const CONFIG_BASENAME` | — | naming the stem every consuming repository declares its build under |
|
|
187
|
+
| `CONFIG_FILENAMES` | `const CONFIG_FILENAMES` | — | listing the file names a configuration may be written as |
|
|
188
|
+
| `findConfigFile` | `findConfigFile(from)` | `string \| undefined` | finding the nearest configuration file at or above a directory |
|
|
189
|
+
| `resolveConfigFile` | `resolveConfigFile(...)` | `{path, fromCwd, fromModule}` | asking which configuration file a build launched here would read, without loading it |
|
|
190
|
+
| `locateConfigError` | `locateConfigError(err, configPath)` | the decorated error | attaching the position of the key a configuration error names |
|
|
191
|
+
| `configFromData` | `configFromData(data, configPath)` | `ContentBuildConfig` | turning a parsed YAML configuration into the frozen one the engine reads |
|
|
192
|
+
| `loadPackConfig` | `loadPackConfig()` | `ContentBuildConfig` | reading the consuming repository's resolved, frozen configuration |
|
|
193
|
+
| `packConfigPath` | `packConfigPath()` | `string` | naming the file `loadPackConfig` resolved the configuration from, for a diagnostic |
|
|
194
|
+
|
|
195
|
+
### `engine.packRouter`
|
|
196
|
+
|
|
197
|
+
Which pack a note's document lands in, when a document type has more than one configured pack — the routing a repository needs once it ships more than one pack per document type (editorial grouping like "Core Spells" and "Expanded Spells").
|
|
198
|
+
|
|
199
|
+
| Export | Signature | Returns | Use it when |
|
|
200
|
+
| ------------------ | -------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
201
|
+
| `PackRoutingError` | `class PackRoutingError extends Error` | — | catching a note that cannot be routed to any configured pack |
|
|
202
|
+
| `PACK_FIELD` | `const PACK_FIELD` | — | naming the frontmatter field a note declares its pack in |
|
|
203
|
+
| `createPackRouter` | `createPackRouter(packs)` | `{resolve, resolveOrNull}` | building a router for one configured pack list, purely, for testing without a config file on disk |
|
|
204
|
+
| `routerFor` | `routerFor(config)` | the router | getting the router for a resolved configuration, built once per configuration |
|
|
205
|
+
| `packRouter` | `packRouter()` | the router | asking, as every module that emits a UUID does, where a note's document lives |
|
|
206
|
+
|
|
207
|
+
### `engine.noteClaims`
|
|
208
|
+
|
|
209
|
+
Which note types a configuration compiles at all, and the two findings that follow: a note no pack would claim, and a note that loses a document while the rest of it compiles.
|
|
210
|
+
|
|
211
|
+
| Export | Signature | Returns | Use it when |
|
|
212
|
+
| -------------------------- | ---------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
213
|
+
| `NEVER_PACKED_TYPES` | `const NEVER_PACKED_TYPES` | — | enumerating note types that compile into no compendium document by design |
|
|
214
|
+
| `DOCUMENTATION_NOTE_TYPES` | `const DOCUMENTATION_NOTE_TYPES` | — | reading the whole note vocabulary a `documentation` package may use — `doc` and `homepage`, both already pages rather than compendium entries |
|
|
215
|
+
| `UNIMPLEMENTED_TYPES` | `const UNIMPLEMENTED_TYPES` | — | enumerating content types the specification states that this toolchain does not yet compile |
|
|
216
|
+
| `DERIVED_PACKED_TYPES` | `const DERIVED_PACKED_TYPES` | — | enumerating note types that reach a pack by a route other than the pack router |
|
|
217
|
+
| `noteTypesClaimedBy` | `noteTypesClaimedBy(docType, sources)` | `ReadonlySet<string>` | listing the note types a pass of one document type would claim |
|
|
218
|
+
| `CLAIMABLE_DOCUMENT_TYPES` | `const CLAIMABLE_DOCUMENT_TYPES` | — | enumerating every Foundry document class `noteTypesClaimedBy` answers for |
|
|
219
|
+
| `documentClassesFor` | `documentClassesFor(type, sources, ...)` | `string[]` | listing every document class a note of one type compiles into |
|
|
220
|
+
| `claimedNoteTypes` | `claimedNoteTypes(config)` | `ReadonlySet<string>` | finding every note type some pack in a configuration would compile |
|
|
221
|
+
| `noteTypeVocabulary` | `noteTypeVocabulary(sources)` | `ReadonlySet<string>` | finding every note type this build knows, whatever one repository configures |
|
|
222
|
+
| `unclaimedNoteFindings` | `unclaimedNoteFindings(config, ...)` | one finding per note | reporting every note in a content tree that no configured pack would compile |
|
|
223
|
+
|
|
224
|
+
### `engine.folderNotes`
|
|
225
|
+
|
|
226
|
+
Folders, authored as notes rather than bespoke `*-folders.yaml` configuration, so a folder's parent is an address the link resolver already understands, its id is derived like any other document's, and an unclaimed folder is a finding like any other unclaimed note.
|
|
227
|
+
|
|
228
|
+
| Export | Signature | Returns | Use it when |
|
|
229
|
+
| ------------------------ | ----------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
230
|
+
| `FOLDER_TYPE` | `const FOLDER_TYPE` | — | naming the note type a folder is authored as |
|
|
231
|
+
| `FOLDER_ID_NAMESPACE` | `const FOLDER_ID_NAMESPACE` | — | naming the id namespace a derived folder id is hashed under |
|
|
232
|
+
| `DEFAULT_PARENT` | `const DEFAULT_PARENT` | — | naming the key a per-pack `parent` map uses for "everywhere else" |
|
|
233
|
+
| `bareAddress` | `bareAddress(value)` | `string \| null` | stripping wikilink brackets and a label off an authored `parent` value |
|
|
234
|
+
| `folderAddress` | `folderAddress(pkg, shortcode)` | `string` — `<pkg>-none-folder-<shortcode>` | composing the canonical address of a folder note |
|
|
235
|
+
| `folderDocId` | `folderDocId(pkg, shortcode)` | `string` — the folder's 16-character Foundry id | deriving the Foundry `_id` a folder note's documents file under |
|
|
236
|
+
| `collectFolderNotes` | `collectFolderNotes(notes, pkg)` | `FolderNote[]` | collecting every folder note in a content tree, in walk order |
|
|
237
|
+
| `buildFolderNoteIndex` | `buildFolderNoteIndex(folders)` | `{byKey, folders, resolve, ancestorsOf, ...}` | indexing folder notes by every form an author may address one by, checking invariants |
|
|
238
|
+
| `folderDocument` | `folderDocument(folder, parent, documentType, stats)` | `object` — the Folder document | compiling the Foundry `Folder` document one folder note produces for one pack |
|
|
239
|
+
| `assertNoDeclaredFolder` | `assertNoDeclaredFolder(fm, ...)` | throws | refusing a note that declares the retired `folder:` spelling |
|
|
240
|
+
|
|
241
|
+
### `engine.contentPackage`
|
|
242
|
+
|
|
243
|
+
Which content package this repository compiles, and which Foundry package ships it — both derived from `package-build.config.yaml`, exposed as the import path link resolution and the compilers have always used.
|
|
244
|
+
|
|
245
|
+
| Export | Signature | Returns | Use it when |
|
|
246
|
+
| ------------------ | -------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
|
247
|
+
| `contentPackage` | `contentPackage()` | `string` — the configured `contentPackage` | naming the address namespace every note in this repository publishes under |
|
|
248
|
+
| `foundryPackageId` | `foundryPackageId()` | `string` — the configured `foundryPackage` | naming the Foundry package these packs ship in and the first segment of every compendium UUID |
|
|
249
|
+
|
|
250
|
+
### `engine.notePackage`
|
|
251
|
+
|
|
252
|
+
Which content package a note belongs to: the repository's configured `contentPackage`, and nothing a note itself may declare — a property of the repository, never a per-note selector.
|
|
253
|
+
|
|
254
|
+
| Export | Signature | Returns | Use it when |
|
|
255
|
+
| ------------------------- | --------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
256
|
+
| `searchableFrontmatter` | `searchableFrontmatter(fm, configured)` | frontmatter with the derived package present | making a note's package visible to a generated table search, though no note declares one |
|
|
257
|
+
| `assertNoDeclaredPackage` | `assertNoDeclaredPackage(fm, ...)` | throws | refusing a note that declares `package:` at all, present or empty |
|
|
258
|
+
|
|
259
|
+
### `engine.retiredFields`
|
|
260
|
+
|
|
261
|
+
Frontmatter fields a note may no longer declare — `draft:`, a top-level `aliases:`, `section:`, a top-level `traits:` block, and any field with a renamed spelling or in-block position — each refused by presence alone, naming the file and line and saying what to write instead.
|
|
262
|
+
|
|
263
|
+
| Export | Signature | Returns | Use it when |
|
|
264
|
+
| ----------------------------- | ------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
265
|
+
| `draftRetiredMessage` | `draftRetiredMessage(file)` | `string` | building the one message shared by the compile-time refusal and the frontmatter lint for `draft:` |
|
|
266
|
+
| `assertNoDraftField` | `assertNoDraftField(fm, ...)` | throws | refusing a note that declares `draft:` at all |
|
|
267
|
+
| `aliasesRetiredMessage` | `aliasesRetiredMessage(file)` | `string` | building the shared message for a top-level `aliases:` |
|
|
268
|
+
| `assertNoAliasesField` | `assertNoAliasesField(fm, ...)` | throws | refusing a note that declares a top-level `aliases:` |
|
|
269
|
+
| `declaresRetiredAliasesField` | `declaresRetiredAliasesField(fm)` | `boolean` | checking whether a note declares the retired top-level `aliases:`, distinct from `name.aliases` |
|
|
270
|
+
| `sectionRetiredMessage` | `sectionRetiredMessage(file)` | `string` | building the shared message for `section:` |
|
|
271
|
+
| `assertNoSectionField` | `assertNoSectionField(fm, ...)` | throws | refusing a note that declares `section:` at all |
|
|
272
|
+
| `traitsRetiredMessage` | `traitsRetiredMessage(file)` | `string` | building the shared message for a top-level `traits:` block, stating where each key moved |
|
|
273
|
+
| `assertNoTraitsField` | `assertNoTraitsField(fm, ...)` | throws | refusing a note that declares a top-level `traits:` block at all |
|
|
274
|
+
| `locateFrontmatterKey` | `locateFrontmatterKey(absPath, key, value, ...)` | `{line?, column?}` | finding a frontmatter key's position in a note's file, by reading it |
|
|
275
|
+
| `RETIRED_FIELD_ALIASES` | `const RETIRED_FIELD_ALIASES` | — | looking up the current field name a retired spelling was renamed to |
|
|
276
|
+
| `retiredAliasMessage` | `retiredAliasMessage(retired, current, file)` | `string` | building the shared message for a note writing a renamed field |
|
|
277
|
+
| `legacyKeyMessage` | `legacyKeyMessage(block, field, file)` | `string` | building the message for a field written at its legacy in-block position |
|
|
278
|
+
| `retiredTopLevelMessage` | `retiredTopLevelMessage(field, file)` | `string` | building the message for a field written at the top-level key `data:` gathered it off |
|
|
279
|
+
| `declaresRetiredAlias` | `declaresRetiredAlias(fm, current)` | `boolean` | checking whether a note writes the retired spelling of a field, in either region `sohlField` reads |
|
|
280
|
+
| `readAliasedField` | `readAliasedField(fm, current)` | the value or `undefined` | reading a field that has a retired spelling, the current name winning |
|
|
281
|
+
|
|
282
|
+
### `engine.runtimeOnlyFields`
|
|
283
|
+
|
|
284
|
+
Schema fields a note may never author, because the document writes them during play rather than at compile time — a permanent part of a type's schema that is simply not content, distinct from a retired field, which a note may not author either but for a different reason.
|
|
285
|
+
|
|
286
|
+
| Export | Signature | Returns | Use it when |
|
|
287
|
+
| --------------------------- | -------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------- |
|
|
288
|
+
| `runtimeOnlyMessage` | `runtimeOnlyMessage(key, field, file)` | `string` | building the shared message every caller reports for an authored runtime-only field |
|
|
289
|
+
| `authoredRuntimeOnlyFields` | `authoredRuntimeOnlyFields(fm, fields, ...)` | `FieldSpec[]` | listing the runtime-only fields a note actually writes, in declaration order |
|
|
290
|
+
| `runtimeOnlyIn` | `runtimeOnlyIn(data, fields)` | `FieldSpec[]` | asking the same question of a `system` block directly, e.g. an actor's embedded item entries |
|
|
291
|
+
| `assertNoRuntimeOnlyFields` | `assertNoRuntimeOnlyFields(fm, fields, ...)` | throws | refusing a note that authors any of its type's runtime-only fields |
|
|
292
|
+
|
|
293
|
+
### `engine.derivedFields`
|
|
294
|
+
|
|
295
|
+
`system` keys a note may never author, because the compiler writes them from the note itself — the third of three refusals that read alike (a note says one thing, the build does another) but state different facts about why the key is off limits.
|
|
296
|
+
|
|
297
|
+
| Export | Signature | Returns | Use it when |
|
|
298
|
+
| ----------------------- | -------------------------------------- | --------------- | ---------------------------------------------------------------------------- |
|
|
299
|
+
| `derivedMessage` | `derivedMessage(key, from, file)` | `string` | building the shared message every caller reports for an authored derived key |
|
|
300
|
+
| `authoredDerivedKeys` | `authoredDerivedKeys(fm, keys, ...)` | `{key, from}[]` | listing the derived keys a note actually writes, in declaration order |
|
|
301
|
+
| `derivedIn` | `derivedIn(data, keys)` | `{key, from}[]` | asking the same question of a `system` block directly |
|
|
302
|
+
| `assertNoDerivedFields` | `assertNoDerivedFields(fm, keys, ...)` | throws | refusing a note that authors any key its compiler derives |
|
|
303
|
+
|
|
304
|
+
### `engine.homepage`
|
|
305
|
+
|
|
306
|
+
The package homepage: a note that compiles to a page rather than a compendium document, hand-authored at the conventional shortcode `root`, checked for its address fields and for uniqueness across the tree.
|
|
307
|
+
|
|
308
|
+
| Export | Signature | Returns | Use it when |
|
|
309
|
+
| ---------------------------- | ------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
310
|
+
| `HOMEPAGE_TYPE` | `const HOMEPAGE_TYPE` | — | naming the note type that compiles to the package homepage |
|
|
311
|
+
| `HOMEPAGE_FIELDS` | `const HOMEPAGE_FIELDS` | — | declaring what a homepage note may write under `sohl:` — nothing |
|
|
312
|
+
| `HOMEPAGE_SHORTCODE` | `const HOMEPAGE_SHORTCODE` | — | naming the shortcode a package landing conventionally takes |
|
|
313
|
+
| `homepageDestination` | `homepageDestination(fm)` | `string` — e.g. `homepage-root.md` | naming the file a homepage is written to, relative to the site root |
|
|
314
|
+
| `isHomepage` | `isHomepage(fm)` | `boolean` | checking whether a note's frontmatter declares the homepage type |
|
|
315
|
+
| `HOMEPAGE_REFUSED_FIELDS` | `const HOMEPAGE_REFUSED_FIELDS` | — | naming the top-level field a homepage refuses, and what it would decide |
|
|
316
|
+
| `checkHomepageAddressFields` | `checkHomepageAddressFields(fm, ...)` | one finding per issue | checking what the address rule says about one homepage note's top-level fields |
|
|
317
|
+
| `checkHomepageCount` | `checkHomepageCount(found, ...)` | one finding per offending note | requiring exactly one homepage note in a content tree |
|
|
318
|
+
| `homepageTitle` | `homepageTitle(fm, config)` | `string` | resolving the title a homepage publishes under, defaulting to the package's manifest title |
|
|
319
|
+
| `homepageFrontmatter` | `homepageFrontmatter(fm, ...)` | `object` | assembling the frontmatter a homepage publishes with, note plus derived values |
|
|
320
|
+
| `HOMEPAGE_ADDRESS_KEYS` | `const HOMEPAGE_ADDRESS_KEYS` | — | naming the two frontmatter keys that hold an address, and what each means |
|
|
321
|
+
| `homepageAddresses` | `homepageAddresses(fm, body, ...)` | `Array<{field, url, kind}>` | finding every address a homepage carries, in frontmatter and body both |
|
|
322
|
+
|
|
323
|
+
### `engine.noteSchemas`
|
|
324
|
+
|
|
325
|
+
The note types the engine itself declares — vocabulary that is a fact about the note format rather than about any game system. `sohl/note-schemas.mjs` (and any other system) declares its own half separately.
|
|
326
|
+
|
|
327
|
+
| Export | Signature | Returns | Use it when |
|
|
328
|
+
| --------------------- | --------------------------- | ------- | --------------------------------------------------------------------------------- |
|
|
329
|
+
| `ENGINE_NOTE_SCHEMAS` | `const ENGINE_NOTE_SCHEMAS` | — | looking up every engine-level content type and what a note of that type may write |
|
|
330
|
+
|
|
331
|
+
### `engine.noteVocabulary`
|
|
332
|
+
|
|
333
|
+
The closed half of a note's frontmatter: the `data:` container and each type's `subType` — the only two regions where an unrecognized key is refused rather than passed through to the page.
|
|
334
|
+
|
|
335
|
+
| Export | Signature | Returns | Use it when |
|
|
336
|
+
| ------------------------- | -------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
337
|
+
| `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
|
|
338
|
+
| `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
|
|
339
|
+
| `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
|
|
340
|
+
| `declaredTags` | `declaredTags(type, groups)` | `readonly string[]` | reading the declared tags a note of a type may carry, flattened |
|
|
341
|
+
| `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
|
|
342
|
+
| `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
|
|
343
|
+
| `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
|
|
344
|
+
| `typeCharsetMessage` | `typeCharsetMessage(type)` | `string` | building the message for a `type` outside the address charset |
|
|
345
|
+
| `assertVocabularyCharset` | `assertVocabularyCharset(vocabulary, where)` | throws | refusing a vocabulary declaration whose type or subType breaks the address charset |
|
|
346
|
+
| `dataFields` | `dataFields(type, vocabulary)` | `readonly DataFieldSpec[] \| undefined` | looking up the `data:` keys a note type may carry |
|
|
347
|
+
| `subTypes` | `subTypes(type, vocabulary)` | `readonly string[] \| null \| undefined` | looking up the closed `subType` values a note type declares |
|
|
348
|
+
|
|
349
|
+
### `engine.systems`
|
|
350
|
+
|
|
351
|
+
The closed registry of system ids, and the `none` that stands for no system at all. An unknown system value is an error; adding a system is a data change to this registry rather than a hardcoded set scattered through the pipeline.
|
|
352
|
+
|
|
353
|
+
| Export | Signature | Returns | Use it when |
|
|
354
|
+
| ---------------------- | -------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
355
|
+
| `NO_SYSTEM` | `const NO_SYSTEM` | — | naming the `<system>` segment of a note that belongs to no game system |
|
|
356
|
+
| `SYSTEM_IDS` | `const SYSTEM_IDS` | — | enumerating every game system this toolchain compiles for |
|
|
357
|
+
| `SYSTEM_SEGMENTS` | `const SYSTEM_SEGMENTS` | — | enumerating everything the `<system>` address segment may say — the systems plus `NO_SYSTEM` |
|
|
358
|
+
| `isSystemId` | `isSystemId(value)` | `boolean` | checking whether a value names a game system this toolchain knows (`none` is rejected) |
|
|
359
|
+
| `isSystemSegment` | `isSystemSegment(value)` | `boolean` | checking whether a value is something the `<system>` address segment may hold |
|
|
360
|
+
| `unknownSystemMessage` | `unknownSystemMessage(value, where)` | `string` | building the message for a caller that wrote an unknown system |
|
|
361
|
+
| `assertSystemSegment` | `assertSystemSegment(value, where)` | `string` — the value, unchanged | refusing a value the `<system>` segment may not hold, validating inline |
|
|
362
|
+
| `assertSystemCharset` | `assertSystemCharset(segments, where)` | throws | refusing a registry declaration whose id could not be an address segment |
|
|
363
|
+
|
|
364
|
+
### `engine.contentAddress`
|
|
365
|
+
|
|
366
|
+
Where a content note publishes on the web. One rule, in one place, because two builds need the same answer: the knowledgebase build renders the page, and the link manifest records the address other packages link to. Stating it twice is how a manifest comes to assert a URL that resolves at build time and 404s for the reader.
|
|
367
|
+
|
|
368
|
+
| Export | Signature | Returns | Use it when |
|
|
369
|
+
| ------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
370
|
+
| `KB_PREFIX` | `const KB_PREFIX` | — | The knowledgebase's mount within this package's site. |
|
|
371
|
+
| `addressSlug` | `function addressSlug(fm)` | {string} The address segment, e.g. | The single path segment a note is addressed by: `type-shortcode`. |
|
|
372
|
+
| `packageAddress` | `function packageAddress(fm)` | {string} The package-relative address, with a trailing slash and no leading one. | A note's address: `<type>-<shortcode>/`, e.g. |
|
|
373
|
+
| `canonicalKey` | `function canonicalKey(pkg, system, type, shortcode)` | {string} `package-system-type-shortcode`, lowercased. | The **canonical** address of a note: fully qualified, one spelling per document, and globally unique. |
|
|
374
|
+
| `blockSystem` | `function blockSystem(keyPath)` | {string} The system id, or `none`. | Which system a frontmatter key path is written under. |
|
|
375
|
+
| `expandAddress` | `function expandAddress(read, where)` | {string} The canonical `package-system-type-shortcode`. | Expand a written address to the one canonical address it names. |
|
|
376
|
+
| `CANONICAL_KEY_SEGMENTS` | `const CANONICAL_KEY_SEGMENTS` | — | How many segments a canonical key has, and therefore how many the reader below counts. |
|
|
377
|
+
| `readCanonicalKey` | `function readCanonicalKey(key)` | {{package: string, system: string, type: string, shortcode: string} \|null\|undefined} The parts; `null` when there is a string that is not in canonical form; `undefined` when there is no key at all. | Reads a canonical key back into its parts. |
|
|
378
|
+
| `PACKAGE_BASE` | `const PACKAGE_BASE` | — | Where this build serves each package, keyed by package name. |
|
|
379
|
+
| `checkBase` | `function checkBase(base, what)` | {string} The base. | Asserts a base is usable as a prefix and returns it. |
|
|
380
|
+
| `packageRelative` | `function packageRelative(url, base)` | {string} The address relative to `base`, with no leading slash. | The package-relative address a site-absolute URL records as. |
|
|
381
|
+
| `resolvePackageUrl` | `function resolvePackageUrl(rel, base)` | {string} The resolved URL. | The URL a package-relative address resolves to in this build. |
|
|
382
|
+
| `DOCUMENT_ID_NAMESPACE` | `const DOCUMENT_ID_NAMESPACE` | — | The namespace {@link documentId} hashes a canonical address under. |
|
|
383
|
+
| `documentId` | `function documentId(pkg, system, type, shortcode)` | {string} A 16-character Foundry id. | The Foundry `_id` of the document a note compiles into, derived from its canonical address. |
|
|
384
|
+
|
|
385
|
+
- `NO_SYSTEM` — re-exported: Re-exported so the address grammar and the system vocabulary are one fact: {@link canonicalKey} writes this segment, and `engine/systems.mjs` decides what may appear in it.
|
|
386
|
+
|
|
387
|
+
### `engine.contentSlug`
|
|
388
|
+
|
|
389
|
+
One normalisation, for every slug this build makes. {@link slugify} reduces a piece of prose — a heading, a document name — to a URL-safe token. It is **not** how a page is addressed: a note's URL is its address, `type-shortcode`, derived in `engine/content-address.mjs` and touching no display string at all.
|
|
390
|
+
|
|
391
|
+
| Export | Signature | Returns | Use it when |
|
|
392
|
+
| --------- | ------------------------ | ------------------------------------------------------------------- | ----------------------------------------------- |
|
|
393
|
+
| `slugify` | `function slugify(text)` | {string} The token, or `""` when the text carries nothing URL-safe. | The URL-safe token a piece of prose reduces to. |
|
|
394
|
+
|
|
395
|
+
### `engine.subtypeRegistry`
|
|
396
|
+
|
|
397
|
+
Which note-type → document-subtype maps this toolchain ships, and the two questions asked of the _list_ rather than of any one map. One frozen list, and the two lookups that need to choose among its members before a map can be consulted at all. It sits here rather than in `note-claims.mjs`, which is where the _questions_ asked of it live — but that module imports half the engine, so anything needing the bare list had to take all of it, and `helpers.mjs` could not take it at all: `note-claims.mjs` imports `walkMarkdownTree` from there, so the dependency would have closed a cycle.
|
|
398
|
+
|
|
399
|
+
| Export | Signature | Returns | Use it when |
|
|
400
|
+
| ----------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
401
|
+
| `KNOWN_DOCUMENT_SUBTYPE_MAPS` | `const KNOWN_DOCUMENT_SUBTYPE_MAPS` | — | The note-type → document-subtype maps this toolchain ships. |
|
|
402
|
+
| `ACTOR_TYPES` | `const ACTOR_TYPES` | — | Every note type any shipped map compiles into an **Actor**. |
|
|
403
|
+
| `subtypeMapFor` | `function subtypeMapFor(system)` | {import("./document-subtypes.mjs").DocumentSubtypeMap\|undefined} Its map, or `undefined` where this toolchain ships none for it. | The map one system ships, by its id. |
|
|
404
|
+
| `schemaSubtypeOf` | `function schemaSubtypeOf(system, type)` | {string} The document subtype to look up. | The document subtype a note type compiles into for one system — the translation the _schema_ check needs, and the reason it needs one. |
|
|
405
|
+
|
|
406
|
+
### `engine.noteIds`
|
|
407
|
+
|
|
408
|
+
The id a note's document is filed under: its pinned `id:`, when a note authors one, or otherwise a hash of its canonical address. The canonical address (`sohl-none-miscgear-bowlcer`) is a readable identity that `content-lint` already guards — a duplicate address is a build error — so the derived id inherits that guarantee. An author who must keep a document's identity stable across a shortcode rename pins an `id:`.
|
|
409
|
+
|
|
410
|
+
| Export | Signature | Returns | Use it when |
|
|
411
|
+
| --------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
412
|
+
| `noteDocId` | `function noteDocId(fm,` | {string\|undefined} The document's `_id`, or `undefined` when the note has no address to derive one from. | The document id a note compiles under: its pin, or its address. |
|
|
413
|
+
| `resolveNoteId` | `function resolveNoteId(fm, opts)` | {object\|null\|undefined} `fm`, for chaining. | Fill a note's `id` in place, so everything downstream reads one value. |
|
|
414
|
+
|
|
415
|
+
### `engine.noteRenames`
|
|
416
|
+
|
|
417
|
+
The shortcodes a note declares it used to be published under. A package's `(type, shortcode)` addresses are a published interface, and `addresses diff` reports what a build stopped publishing — telling a **rename** from a **withdrawal** by matching document ids across two releases. The property that rested on is gone: an id is derived from the canonical address, which carries the shortcode, so renaming a shortcode moves the id too: both sides of the join move together, the match finds nothing, and a rename is reported as a withdrawal with no successor named.
|
|
418
|
+
|
|
419
|
+
| Export | Signature | Returns | Use it when |
|
|
420
|
+
| --------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
|
|
421
|
+
| `renamedFromEntries` | `function renamedFromEntries(fm)` | {readonly unknown[]} The authored entries, in authored order; empty when the note declares none. | The `renamedFrom:` entries a note authors, exactly as authored. |
|
|
422
|
+
| `renamedFrom` | `function renamedFrom(fm)` | {string[]} The declared predecessor shortcodes. | The well-formed shortcodes among a note's `renamedFrom:` entries. |
|
|
423
|
+
| `declaresRenamedFrom` | `function declaresRenamedFrom(fm)` | {boolean} `true` when the key is present and not null. | Whether a note declares the key at all, however malformed its value. |
|
|
424
|
+
|
|
425
|
+
### `engine.metadataIndex`
|
|
426
|
+
|
|
427
|
+
The published content index — the artifact packages exchange addresses through. **A package publishes its own index; a consumer fetches the ones it depends on.** That is the whole mechanism, and it replaces a vendored link manifest that each repository committed a copy of every other repository's file into. Vendoring failed three ways, and only the last is about staleness:
|
|
428
|
+
|
|
429
|
+
| Export | Signature | Returns | Use it when |
|
|
430
|
+
| ------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
431
|
+
| `METADATA_RELATIONSHIP_KINDS` | `const METADATA_RELATIONSHIP_KINDS` | — | The relationship kinds that are dependencies, and therefore citable. |
|
|
432
|
+
| `metadataFileName` | `function metadataFileName(pkg)` | {string} The file name, e.g. | What a package's content index is called, wherever it is written or fetched. |
|
|
433
|
+
| `metadataRelationships` | `function metadataRelationships(config)` | {Array<{id: string, manifest: string, kind: string, verified: string\|undefined}>} The dependencies, in declaration order. | Every dependency whose published index this build resolves addresses through. |
|
|
434
|
+
| `metadataCacheDir` | `function metadataCacheDir(config, id, version)` | {string} The directory. | The cache directory for one dependency's index at one version. |
|
|
435
|
+
| `isComplete` | `const isComplete` | {boolean} True when it was fetched to completion. | Whether a dependency's cache is present and complete. |
|
|
436
|
+
| `markComplete` | `function markComplete(dir)` | {void} | Mark a dependency's cache complete. |
|
|
437
|
+
| `cachedMetadataFiles` | `function cachedMetadataFiles(config)` | {string[]} One index file per declared dependency. | The fetched index files this build resolves foreign addresses against. |
|
|
438
|
+
| `cachedMetadataIndexes` | `function cachedMetadataIndexes(config)` | {Array<{id: string, file: string}>} One entry per declared dependency. | The same fetched indexes, each paired with the package that published it. |
|
|
439
|
+
| `newestVersionDir` | `function newestVersionDir(dirs)` | {string} The newest one. | The newest cached version among several version-keyed cache directories. |
|
|
440
|
+
| `loadForeignIndexes` | `function loadForeignIndexes(config, localPackages, bases` | {{index: Map<string, object>, packages: Set<string>, stale: Array<{package: string, reason: string}>}} The resolved addresses, which packages contributed, and what could not be read. | Resolve every foreign address this build can cite, from the fetched indexes. |
|
|
441
|
+
| `cachedIndexPath` | `function cachedIndexPath(config, pkg)` | {string} A path to name in a diagnostic. | Where a dependency's fetched index sits, for naming it in a diagnostic. |
|
|
442
|
+
| `unaddressableForeignPackages` | `function unaddressableForeignPackages(foreignIndex)` | {Array<{package: string, entries: number, sampleKey: string}>} One finding per drifted package, in the order the index first names each. | Whether a fetched index can still be _addressed_, as distinct from read. |
|
|
443
|
+
| `formatUnaddressableFinding` | `function formatUnaddressableFinding(finding, config)` | {string} The formatted diagnostic, path first on the line. | One finding, in the standard `file:line:column: severity: message` form. |
|
|
444
|
+
|
|
445
|
+
### `engine.foundryEntries`
|
|
446
|
+
|
|
447
|
+
Emitting this package's cross-package link manifest. `engine/content-address.mjs` owns the address _grammar_ — how a key is version is read, how a foreign file resolves. This module owns the _pass_: walking a content tree and deriving, for every note it publishes, the addresses that entry states. The two halves were split across the format module and a hand-written script in each consuming repository, which is how the two scripts came to differ in ways nobody chose — one routes its UUIDs through the pack router and one does not, and neither knew.
|
|
448
|
+
|
|
449
|
+
| Export | Signature | Returns | Use it when |
|
|
450
|
+
| ----------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
451
|
+
| `LEAD_ANCHOR` | `const LEAD_ANCHOR` | — | The reserved anchor name for a journal's **first** page. |
|
|
452
|
+
| `anchorsOf` | `function anchorsOf(entryUuid, entryId, body, name)` | {Record<string, string>} The anchors. | Every page of a note's journal, as `anchorName → whole UUID`. |
|
|
453
|
+
| `entriesForNote` | `function entriesForNote(fm, name, address, body, ctx)` | {Array<object>} One or two entries, in {@link buildManifest}'s shape. | The manifest entries a single note produces. |
|
|
454
|
+
| `collectFoundryEntries` | `function collectFoundryEntries(contentBase, ctx)` | {{entries: Array<object>, notes: number, skipped: Array<{file: string, reason: string}>}} | Every note this package publishes, as manifest entries. |
|
|
455
|
+
| `foundryIdentities` | `function foundryIdentities(config` | {{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}} | The identities an emission runs against, from configuration. |
|
|
456
|
+
| `entryContext` | `function entryContext(config` | {{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}} | The identities an emission runs against, from configuration. |
|
|
457
|
+
|
|
458
|
+
### `engine.contentIndex`
|
|
459
|
+
|
|
460
|
+
Emitting this package's content index. Every content build already walks the whole note tree and parses every note's frontmatter — the pack compilers, the site build, and the content-table expander each do it — and every one of them throws the result away when it finishes. So nothing outside a build can ask a question about the content: "which beings carry no `kbcat`?", "what does this table actually select?", "did that type rename leave anything behind?" have no answer short of writing a throwaway script that re-walks the tree. Eight dead Bestiary tables shipped for weeks behind exactly that gap.
|
|
461
|
+
|
|
462
|
+
| Export | Signature | Returns | Use it when |
|
|
463
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
464
|
+
| `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
|
|
465
|
+
| `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
|
|
466
|
+
| `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
|
|
467
|
+
| `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
|
|
468
|
+
| `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
|
|
469
|
+
| `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
|
|
470
|
+
| `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | {{file: string, notes: number, bytes: number}} Where it was written, how many notes it holds, and its size. | Emit this package's content index. |
|
|
471
|
+
| `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
|
|
472
|
+
| `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, manifest, problems })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's) |
|
|
473
|
+
|
|
474
|
+
`engine.contentIndex` also re-exports five names from neighbouring leaf modules, at this same import path:
|
|
475
|
+
|
|
476
|
+
| Export | Signature | Returns | Use it when |
|
|
477
|
+
| --------------------- | -------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
478
|
+
| `collectAnchors` | `collectAnchors(body, bodyLine)` | `Array<{slug: string, line: number}>` | reading a note body's anchors, shared with the link checker and every build that emits a link |
|
|
479
|
+
| `DERIVED_KEYS` | `const DERIVED_KEYS` | — | reading which keys the content index adds to a record, and which a note therefore may not author itself |
|
|
480
|
+
| `noteFile` | `noteFile(contentBase, record)` | `string` — the note's absolute path | composing an index record's `file.path` (recorded relative, for a byte-stable artifact) back into an openable absolute path |
|
|
481
|
+
| `authoredFrontmatter` | `authoredFrontmatter(record)` | `Record<string, any>` — the frontmatter without `DERIVED_KEYS` | reading back exactly what a note authored from its index record, so a pass can lint or compile from the index without reasoning about derived fields as if the author wrote them |
|
|
482
|
+
| `isNoteRecord` | `isNoteRecord(record)` | `boolean` | telling a note's own record apart from its documentation journal's, when enumerating the corpus |
|
|
483
|
+
|
|
484
|
+
### `engine.siteBuild`
|
|
485
|
+
|
|
486
|
+
Publishing a content tree as a website. Compiling a content tree into compendium packs is `content-build package compile`. Publishing the _same tree_ as a website was a script each consumer wrote for itself — 473 code lines in `sohl` and 462 in `sohl-thalorna`, 87 of them identical — and the copies drifted in ways neither repository could see. `sohl-thalorna` reimplemented four things this package already exported, not because it needed different behaviour but because its script predates the extraction. That is the failure a command removes: a consumer cannot accidentally reimplement one.
|
|
487
|
+
|
|
488
|
+
| Export | Signature | Returns | Use it when |
|
|
489
|
+
| ---------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
490
|
+
| `walkSiteTree` | `function walkSiteTree(dir, skip` | {string[]} Absolute paths. | Every `.md` file under `dir`, depth-first in directory order. |
|
|
491
|
+
| `collectContentPages` | `function collectContentPages(contentBase, ctx)` | {{pages: object[], addressFindings: object[], fmLinkFindings: object[]}} | The content tree's pages, and what could not be addressed. |
|
|
492
|
+
| `collectTreePages` | `function collectTreePages(tree, ctx)` | {{pages: object[], fmLinkFindings: object[]}} | An extra tree's pages — a documentation tree published alongside the content. |
|
|
493
|
+
| `collectHomepages` | `function collectHomepages(contentBase, ctx)` | {{pages: object[], addressFindings: object[]}} The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
|
|
494
|
+
| `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage at its address, below the package's own root. |
|
|
495
|
+
| `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
|
|
496
|
+
| `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
|
|
497
|
+
| `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
|
|
498
|
+
| `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
|
|
499
|
+
| `sectionFrontmatter` | `function sectionFrontmatter(meta)` | {object} Its front matter, `title` first. | The front matter a section's landing states about itself. |
|
|
500
|
+
| `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
|
|
501
|
+
| `pageDestination` | `function pageDestination(page)` | — | Where a page is written, relative to the output root. |
|
|
502
|
+
| `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
|
|
503
|
+
| `writeSectionLandings` | `function writeSectionLandings(outRoot,` | {number} How many landings were written. | Writes the Hugo sections a published tree declares. |
|
|
504
|
+
| `pluralTitle` | `function pluralTitle(name)` | {string} The display title. | A section landing's title, from its directory name — `macro` → `Macros`. |
|
|
505
|
+
| `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function, afterLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
|
|
506
|
+
| `resolveOutputRoot` | `function resolveOutputRoot(rootDir, out)` | {string} The absolute output root. | The output root, having established that it is safe to delete. |
|
|
507
|
+
| `buildSite` | `buildSite({ config, outRoot, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
|
|
508
|
+
|
|
509
|
+
### `engine.contentLint`
|
|
510
|
+
|
|
511
|
+
Linting a content tree's **addresses** — the rules every package's notes are authored against, wherever those notes live. These rules do not live in a consumer's `utils/`, which has two consequences and no upside. `thalorna` and `kethira` notes were checked by nothing at all, so the packages most likely to carry authoring mistakes were the ones nothing inspected. And one rule with two implementations can disagree without anything detecting it, which the canonical-separator handling already did once on each side.
|
|
512
|
+
|
|
513
|
+
| Export | Signature | Returns | Use it when |
|
|
514
|
+
| ------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
515
|
+
| `SHORTCODE_PATTERN` | `const SHORTCODE_PATTERN` | — | The shape every `shortcode` must match: ASCII letters and digits only. |
|
|
516
|
+
| `isValidShortcode` | `function isValidShortcode(value)` | {boolean} `true` when it matches {@link SHORTCODE_PATTERN}. | Whether a value is a well-formed shortcode. |
|
|
517
|
+
| `lintContentTree` | `lintContentTree(contentBase, { skipDirectories, contentPackage, config, records, problems })` | {{findings: Array<{file: string, line?: number, column?: number, severity: "error"\|"warning", message: string}>, notes: number, keys: number}} The findings, and what was inspected to produce them. | Lint every address in a content tree. |
|
|
518
|
+
|
|
519
|
+
### `engine.contentCharset`
|
|
520
|
+
|
|
521
|
+
The charset authored content is held to, so a book can choose its face. The packs and the website render in whatever font the reader's browser or Foundry supplies, and a glyph nobody has is somebody else's problem. **A book is not that.** A PDF embeds the faces it sets, so every character in the corpus is a claim on the book's typeface — and the claim is silent, which is what makes it expensive.
|
|
522
|
+
|
|
523
|
+
| Export | Signature | Returns | Use it when |
|
|
524
|
+
| -------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
525
|
+
| `isLetterTier` | `function isLetterTier(cp)` | {boolean} Whether Tier 1 admits it. | Tier 1 — the letters, and the two whitespace characters a file is made of. |
|
|
526
|
+
| `TYPOGRAPHY` | `const TYPOGRAPHY` | — | Tier 2 — typography, enumerated one codepoint at a time. |
|
|
527
|
+
| `NOTATION` | `const NOTATION` | — | Tier 3 — the notation the rules and price tables are written in. |
|
|
528
|
+
| `isAllowedCodePoint` | `function isAllowedCodePoint(cp)` | {boolean} Whether it is allowed outside a code fence. | Whether the charset admits a code point anywhere in a note. |
|
|
529
|
+
| `isDiagramCodePoint` | `function isDiagramCodePoint(cp)` | {boolean} Whether a fence may carry it. | Whether a code point is diagram furniture, admitted inside a fence only. |
|
|
530
|
+
| `refusalFor` | `function refusalFor(cp)` | {string} A clause naming what it is and what to do instead. | The reason a code point is refused. |
|
|
531
|
+
| `decomposedRuns` | `function decomposedRuns(text)` | {Array<{sequence: string, composed: string, index: number}>} Each offending run, in the order it appears. | Every non-NFC run in a string, with the composed form it should have been. |
|
|
532
|
+
| `checkText` | `function checkText(text, file)` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} What is wrong, in file order. | Check one file's text against the charset and the normalization rule. |
|
|
533
|
+
| `lintContentCharset` | `function lintContentCharset(contentBase,` | {{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}} The findings, and how many files produced them. | Walk a content tree and check every authored file in it. |
|
|
534
|
+
|
|
535
|
+
### `engine.contentIcons`
|
|
536
|
+
|
|
537
|
+
Naming an interface icon in a note, without drawing it there. The user guide describes Foundry's interface, and it did so by pasting Unicode lookalikes of the icons the sheets actually draw: `☆` for the improve flag, `✎` for the formula editor, `◆` in the success-value table, `★★★` for mastery. The system renders every one of those with **Font Awesome** — a `fa-regular fa-star`, a `fa-solid fa-pen-to-square` — so the note and the screen it describes were drawing different pictures, and drifting apart with every sheet change.
|
|
538
|
+
|
|
539
|
+
| Export | Signature | Returns | Use it when |
|
|
540
|
+
| --------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
541
|
+
| `EMPTY_ICON_REGISTRY` | `const EMPTY_ICON_REGISTRY` | — | The registry a package that declares none gets: nothing at all. |
|
|
542
|
+
| `familyOf` | `function familyOf(entry, registry` | {string\|undefined} The family name, or nothing when neither the entry nor the registry says. | The family an entry draws from, named or defaulted. |
|
|
543
|
+
| `ICON_SIZES` | `const ICON_SIZES` | — | The sizes a note may ask for, and what each means on a page. |
|
|
544
|
+
| `ICON_ATTRIBUTES` | `const ICON_ATTRIBUTES` | — | The attribute names a note may write, and how each is validated. |
|
|
545
|
+
| `parseIconAttributes` | `function parseIconAttributes(raw)` | {{attrs: Record<string, string>, problems: string[]}} What was written, and what cannot be honoured. | Read the brace of an icon token. |
|
|
546
|
+
| `ICON_PATTERN` | `const ICON_PATTERN` | — | The shape a note writes, and the one this module claims. |
|
|
547
|
+
| `resolveIcon` | `function resolveIcon(name, registry` | {object\|null} The entry, or `null` when the registry does not declare it. | Look one name up. |
|
|
548
|
+
| `iconHtml` | `function iconHtml(entry, attrs` | {string} An `<i>` element. | The HTML the journals and the website emit — what the system already renders. |
|
|
549
|
+
| `iconsIn` | `function iconsIn(text)` | {Array<{name: string, index: number, raw: string}>} What it names. | Every icon a string names, in the order written. |
|
|
550
|
+
| `lintIcons` | `function lintIcons(text, file, registry` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} The unknown names. | Report every icon a tree names that its registry does not declare. |
|
|
551
|
+
| `checkIconRegistry` | `function checkIconRegistry(registry, where` | {Array<{severity: "warning", message: string}>} What is wrong with it. | What is wrong with a package's declared registry. |
|
|
552
|
+
| `lintContentIcons` | `function lintContentIcons(contentBase,` | {{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}} What it found. | Walk a content tree and report every icon name its registry does not declare. |
|
|
553
|
+
| `iconPlugin` | `function iconPlugin(registry` | {(md: object) => void} A markdown-it plugin. | A markdown-it plugin rendering `:icon-name:` inline. |
|
|
554
|
+
|
|
555
|
+
### `engine.contentHtml`
|
|
556
|
+
|
|
557
|
+
Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cannot say, a note does not say — it gets a construct every surface can render, the way `:icon-…:` replaces a pasted glyph.
|
|
558
|
+
|
|
559
|
+
| Export | Signature | Returns | Use it when |
|
|
560
|
+
| ----------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
|
|
561
|
+
| `HTML_TAG` | `const HTML_TAG` | — | A raw HTML tag, opening, closing or self-closing. |
|
|
562
|
+
| `htmlMessage` | `function htmlMessage(tag)` | {string} The message, unpunctuated at the end as a finding is. | What a note carrying raw HTML is told. |
|
|
563
|
+
| `checkHtml` | `function checkHtml(body, file,` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} One finding per tag, in source order. | Every raw HTML tag in one note's body. |
|
|
564
|
+
| `lintContentHtml` | `function lintContentHtml(contentBase,` | {{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}} The findings, and how many files were read. | Walk a content tree and report raw HTML in every note's prose. |
|
|
565
|
+
|
|
566
|
+
### `engine.contentLinks`
|
|
567
|
+
|
|
568
|
+
Resolving every link in a content tree, and reporting the ones that land nowhere. Three link defects survive both content builds silently, so neither the pack compilers nor a site build catches them:
|
|
569
|
+
|
|
570
|
+
| Export | Signature | Returns | Use it when |
|
|
571
|
+
| -------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
572
|
+
| `anchorsOf` | `function anchorsOf(body)` | {Set<string>} The declared anchor slugs. | Every `{#anchor}` a note declares on a heading. |
|
|
573
|
+
| `buildLinkIndex` | `buildLinkIndex(contentBase, { config, skipDirectories, sqlTables, records, problems })` | {object} The notes, the index, and the resolvers built over it. | Read a content tree into the index a link resolves against. |
|
|
574
|
+
| `auditHomepageLinks` | `function auditHomepageLinks(index)` | {Array<{note: object, field: string, url: string, text: string, occurrence: number, message: string}>} One finding per defect, `text` and `occurrence` locating it in the note's raw source. | Every defect in the addresses a package homepage carries. |
|
|
575
|
+
| `auditLinks` | `function auditLinks(index)` | {{deadAnchors: object[], deadAddresses: object[], unlabelledLinks: object[], frontmatterLinks: object[], homepageLinks: object[], usedManifest: Set<string>}} The findings, and which addresses a foreign manifest answered. | Every link in a tree that lands nowhere. |
|
|
576
|
+
| `walkReachability` | `function walkReachability(index,` | {{root: object, reached: Set<object>, orphans: object[]}} The root, everything reached from it, and the corpus members that were not. | Walk a corpus from its root and report what nothing links to. |
|
|
577
|
+
|
|
578
|
+
### `engine.webWikilinks`
|
|
579
|
+
|
|
580
|
+
Wikilink resolution for the knowledgebase build. The same authored links the pack compilers turn into Foundry `@UUID` enrichers (see `./wikilinks.mjs`) become site-local hrefs here:
|
|
581
|
+
|
|
582
|
+
| Export | Signature | Returns | Use it when |
|
|
583
|
+
| ---------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
584
|
+
| `frontmatterWikilinks` | `function frontmatterWikilinks(fm)` | {Array<{path: string, link: string}>} In reading order; `path` is the dotted key path of the offending value (`government.summary`, `aliases.1`). | Every wikilink authored inside a frontmatter value. |
|
|
585
|
+
| `resolveWebWikilinks` | `function resolveWebWikilinks(body, ctx)` | {string} The body with wikilinks rewritten. | Rewrites the wikilinks in a markdown body as KB-local markdown links. |
|
|
586
|
+
|
|
587
|
+
### `engine.contentTables`
|
|
588
|
+
|
|
589
|
+
Generated content tables — Obsidian **Dataview** `TABLE` queries. A catalog table (every cloth armour, every animal's abilities) is data that already lives in the frontmatter of the notes it describes. Authoring such a table by hand duplicates that data and guarantees drift, so a content body instead declares what it wants tabulated, in a fenced `dataview` block:
|
|
590
|
+
|
|
591
|
+
| Export | Signature | Returns | Use it when |
|
|
592
|
+
| --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
593
|
+
| `parseDataviewQuery` | `function parseDataviewQuery(source)` | {{columns: Array<{header: string, expr: object}>, from: object\|null, where: object\|null, sort: Array<{expr: object, descending: boolean}>, limit: number\|null}} | Parse one `dataview` block's query. |
|
|
594
|
+
| `resolveField` | `function resolveField(doc, path)` | {unknown} `null` when the note has no such field. | Resolve a dotted path against a content note. |
|
|
595
|
+
| `evaluate` | `function evaluate(node, doc, self)` | {unknown} | Evaluate one parsed expression against a note. |
|
|
596
|
+
| `selectRows` | `function selectRows(spec, docs, self)` | {Array<ContentTableDoc>} The matching notes, sorted and limited. | The notes a query selects, in the order its `SORT` keys give. |
|
|
597
|
+
| `renderContentTable` | `function renderContentTable(spec, rows, linkable, self)` | {string} The markdown table (no trailing newline). | Build the markdown table for one query. |
|
|
598
|
+
| `expandContentTables` | `expandContentTables(markdown, { docs, linkable, source, self, sqlTables })` | `{markdown: string, errors: object[], lineMap: object[], warnings: object[]}` | expanding every fenced `dataview` block in a markdown body; a block that cannot be honoured is left verbatim and reported in `errors` rather than failing the build, and a query matching no note renders as an empty table |
|
|
599
|
+
|
|
600
|
+
### `engine.helpers`
|
|
601
|
+
|
|
602
|
+
Shared helpers for the pack compilers in `packages/content-build/`. The HeroicLands vault is authoritative for compendium item data. Pack compilers walk the vault, read markdown files with YAML frontmatter, and emit Foundry-compatible JSON. These helpers handle the common shape: markdown parsing, frontmatter access (including the nested `sohl:` block), filename generation, and slug normalization.
|
|
603
|
+
|
|
604
|
+
| Export | Signature | Returns | Use it when |
|
|
605
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
606
|
+
| `md` | `const md` | — | The markdown renderer every surface shares. |
|
|
607
|
+
| `parseMarkdownFile` | `function parseMarkdownFile(filePath)` | — | Parses a markdown file with YAML frontmatter. |
|
|
608
|
+
| `assertStatedScope` | `function assertStatedScope(skipDirectories, who)` | {void} | Refuse a corpus read whose scope its caller did not state. |
|
|
609
|
+
| `assertSuppliedCorpus` | `function assertSuppliedCorpus(records, who)` | {void} | Refuse a corpus read whose records its caller did not supply. |
|
|
610
|
+
| `statedTemplatePriority` | `function statedTemplatePriority(fm, label,` | {number\|null} The priority, or `null` when the note is not a template or states nothing. | The template priority a note states, for a system that treats an unstated one as "not a template" rather than as an authoring error. |
|
|
611
|
+
| `resolveTemplatePriority` | `function resolveTemplatePriority(fm, label,` | {number\|undefined} The template priority, or `undefined` when null. | Resolve the required `templatePriority` frontmatter for an Item/Actor entry (the archetype contract). |
|
|
612
|
+
| `systemTemplatePriority` | `function systemTemplatePriority(fm, label)` | {number\|null} The template priority, or `null` for a document that is not a template. | The value a document's `system.templatePriority` carries, from the required `templatePriority` frontmatter (`archetype` is the legacy spelling). |
|
|
613
|
+
| `makeFilename` | `function makeFilename(name, id)` | — | Generates a compendium-source filename: `Name_id.json` with non- alphanumeric runs replaced by underscores. |
|
|
614
|
+
| `resolveImg` | `function resolveImg(raw, config` | {string \| null} the Foundry-relative path; `""` for a deliberate blank, and `null` when the note names no art at all. | Translate a content-relative image path into its Foundry-relative form. |
|
|
615
|
+
| `resolveName` | `function resolveName(fm, defaultValue` | — | Resolves the display name from frontmatter, preferring `name.full`, falling back to `name` (if string), then `defaultValue`. |
|
|
616
|
+
| `supportedCoreVersion` | `function supportedCoreVersion(config` | {string} The declared `compatibility.minimum`. | The oldest Foundry core this package supports, stamped into every compiled document as `_stats.coreVersion`. |
|
|
617
|
+
| `buildStats` | `function buildStats(systemVersion` | {object} The `_stats` block. | Default `_stats` block for compiled compendium entries. |
|
|
618
|
+
| `statsForPack` | `function statsForPack(packSystem, config` | {object} The `_stats` block for that pack. | The `_stats` block for one pack, stamped with the system that pack is for. |
|
|
619
|
+
| `defaultStats` | `function defaultStats()` | {object} The default `_stats` block, shared by every compiler. | The `_stats` block every compiler stamps on an entry it emits, built once. |
|
|
620
|
+
| `buildContentLinkIndex` | `buildContentLinkIndex(contentBase, router, { skipDirectories, config, records, problems })` | {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`. | Indexes **every** note in the content tree so any pack compiler can resolve a wikilink to any other document. |
|
|
621
|
+
| `convertNoteWikilinks` | `convertNoteWikilinks(body, { type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap })` | {{markdown: string, unresolved: Array<object>}} | Converts the wikilinks in one note's markdown, reporting any that have no target in the content tree. |
|
|
622
|
+
| `collectContentDocs` | `collectContentDocs(contentBase, { skipDirectories, config, records, problems })` | {Array<{fm: object, path: string, tld: string, folder: string, absPath: string}>} | Every note in the content tree, in the shape the `dataview` table expander searches: its frontmatter plus where it sits in the tree. |
|
|
623
|
+
| `expandNoteTables` | `function expandNoteTables(body,` | {{markdown: string, lineMap: Array<{line: number, generated: boolean}>}} The body with every table expanded, and where each emitted line came from — which is what lets a diagnostic about the expanded body name an authored position. | Expand the fenced `dataview` tables in one note's markdown, before wikilinks are resolved — so a generated cell may itself be a wikilink. |
|
|
624
|
+
| `folderFilename` | `function folderFilename(name, id)` | — | Builds a compendium-source filename for a folder JSON document: `folder_Name_id.json` with non-alphanumeric runs replaced by underscores. |
|
|
625
|
+
| `walkMarkdownTree` | `walkMarkdownTree(rootDir, { skipDirectories })` | generator yielding `{frontmatter, body, description, bodyLine?, bodyColumn?, file, absPath}` | walking a content tree and reading every markdown file's frontmatter and body; `skipDirectories` is required rather than defaulted, so two callers cannot silently disagree about the corpus's scope |
|
|
626
|
+
|
|
627
|
+
### `engine.itemRegistry`
|
|
628
|
+
|
|
629
|
+
**The resolved item-type registry** — the consuming repository's `itemBuilders` table, and the type whitelist derived from its keys. Both are read from the one resolved configuration, so they are literally the same object's keys and values: a type cannot be whitelisted for compilation without the builder that compiles it, which is the guarantee this exists for. Where a consumer declares **several** registries, one per system, the vocabulary is their union and every lookup below takes the system that is asking — a type both systems declare has two builders, and answering with one of them because it was declared first is the silent-wrong-output failure this package spends its time removing. The Item compiler dispatches through {@link itemBuilder}, so the table a consumer configured is the table its notes compile with — the whitelist and the dispatch would otherwise come from different places, and a consumer supplying its own registry got the types it asked for and the builders it did not.
|
|
630
|
+
|
|
631
|
+
| Export | Signature | Returns | Use it when |
|
|
632
|
+
| ------------- | ------------------------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
633
|
+
| `itemTypes` | `function itemTypes()` | {ReadonlySet<string>} The configured item types. | Every content type that compiles into an item — and therefore into an item doc. |
|
|
634
|
+
| `itemBuilder` | `function itemBuilder(type, system)` | {(fm: object) => object} The builder for that type. | The builder the consuming repository registered for an item type. |
|
|
635
|
+
| `itemFields` | `function itemFields(type, system)` | {readonly object[]\|undefined} The declaration, or `undefined`. | The frontmatter fields a type's registry entry declares, if any. |
|
|
636
|
+
| `itemArt` | `function itemArt(type, system)` | {string} The default image path for that type. | The default art for an item type — the image a note of that type is given when it carries no `img:` of its own. |
|
|
637
|
+
|
|
638
|
+
### `engine.documentSubtypes`
|
|
639
|
+
|
|
640
|
+
**The note-type → document-subtype map** — the mechanism that stops a build inferring a Foundry document's subtype from the markdown note's `type`. A note's `type` and the subtype of the document it compiles into are two vocabularies, and until now they were the same identifier for one reason only: a builder wrote the same string twice. `sohl/actors.mjs` declared `ACTOR_VAULT_TYPE = "being"` and emitted `type: "being"` several hundred lines below it, under a comment reading _"One content type, named for the Foundry actor it produces."_ Nothing related the two, so changing one and not the other produced a wrongly-typed document in silence — a wrong-output risk with **one** system, not merely with two.
|
|
641
|
+
|
|
642
|
+
| Export | Signature | Returns | Use it when |
|
|
643
|
+
| ------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
644
|
+
| `defineDocumentSubtypes` | `defineDocumentSubtypes({ system, block, types })` | {DocumentSubtypeMap} The frozen map, rows and all. | Declare one system's note-type → document-subtype map. |
|
|
645
|
+
| `subtypeRow` | `function subtypeRow(map, noteType)` | {Readonly<DocumentSubtypeRow>\|undefined} The row, or `undefined` where this system maps the type at all. | The row a system declares for a note type, or nothing. |
|
|
646
|
+
| `mapsNoteType` | `function mapsNoteType(map, noteType, document)` | {boolean} True when the map carries a matching row. | Whether a system maps a note type — optionally, onto one document class. |
|
|
647
|
+
| `noteTypesFor` | `function noteTypesFor(map, document)` | {string[]} The note types, in sorted order. | Every note type a system maps onto one document class, sorted. |
|
|
648
|
+
| `documentSubtype` | `function documentSubtype(map, noteType, fm,` | {string\|undefined} The subtype, or `undefined` where this system maps nothing for the type — which means no document, not an error. | The document subtype a note compiles into for one system. |
|
|
649
|
+
| `referencedSubtype` | `function referencedSubtype(map, noteType, document)` | {ReferencedSubtype} The subtype, or why there is none. | The document subtype a `(type, shortcode)` **reference** addresses. |
|
|
650
|
+
| `systemOf` | `function systemOf(type, maps)` | {string} The system id, or {@link NO_SYSTEM}. | Which system defines the document a note of this type compiles into. |
|
|
651
|
+
|
|
652
|
+
### `engine.itemDocs`
|
|
653
|
+
|
|
654
|
+
**Item docs** — an item's prose compiled as a JournalEntry, with the item keeping only a pointer to it. An item note's body describes what the thing _is_. That is documentation, and documentation belongs in the journals pack, so each item note compiles into a JournalEntry and the item's `system.docHtml` becomes nothing but a `@UUID` link to that entry's first page — the description-as-pointer convention, which {@link sohl.utils.descriptionLinkTarget} recognises and Display Description follows.
|
|
655
|
+
|
|
656
|
+
| Export | Signature | Returns | Use it when |
|
|
657
|
+
| ---------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
|
658
|
+
| `docEntryTypes` | `function docEntryTypes()` | {ReadonlySet<string>} The configured doc-carrying types. | Every content type whose **prose compiles into a JournalEntry of its own**, addressed by the virtual `doc<type>` qualifier. |
|
|
659
|
+
| `hasDocEntry` | `function hasDocEntry(type)` | {boolean} True for an item type, for `macro` and for a map type; false for `doc` and for actors. | Whether a content note's type is one whose prose becomes a JournalEntry of its own. |
|
|
660
|
+
| `itemDocEntryId` | `function itemDocEntryId(itemId)` | {string} A 16-character Foundry id. | The id of the JournalEntry a note's prose compiles into — an item's, or a macro's. |
|
|
661
|
+
| `itemDocPointer` | `function itemDocPointer(packageId, itemId, name, firstPageId)` | {string} The pointer to store in `system.docHtml`. | The description an item carries in place of its prose: a `@UUID` link to the first page of its item doc, and nothing else. |
|
|
662
|
+
|
|
663
|
+
- `itemTypes` — re-exported: Every content type that compiles into an item — and therefore into an item doc.
|
|
664
|
+
|
|
665
|
+
### `engine.wikilinks`
|
|
666
|
+
|
|
667
|
+
Wikilink resolution for the pack compilers. Content notes link to one another with wikilinks rather than file paths:
|
|
668
|
+
|
|
669
|
+
| Export | Signature | Returns | Use it when |
|
|
670
|
+
| -------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
671
|
+
| `resolveItemDocType` | `function resolveItemDocType(qualifier, types)` | {string\|null} The underlying document type, or `null` when the qualifier is not a virtual one. | Reads a qualifier as the **virtual `doc<type>`** form, or reports that it is not one. |
|
|
672
|
+
| `readQualifier` | `function readQualifier(target, types, packages)` | {{type: string, shortcode: string, itemDoc: boolean, package?: string, system?: string, reason?: undefined} \| {reason: "unknown-type"} \| null} The resolved qualifier; a `reason` when the target is definitely qualified but names no known type; or `null` when it is not an address at all. | Read a link target as a **qualified** `type-shortcode` reference, or report that it does not parse as one. |
|
|
673
|
+
| `anchorPageId` | `function anchorPageId(noteId, anchorSlug)` | {string} A 16-character alphanumeric id. | The deterministic JournalEntryPage id for one anchor: SHA-256 of `"<noteId>-<anchorSlug>"`, base64-encoded, reduced to the 16 alphanumeric characters a Foundry id allows. |
|
|
674
|
+
| `buildWikilinkIndex` | `function buildWikilinkIndex(docs, packageId, foreign, contentPackage)` | {{byShortcode: Map<string, object>, types: Set<string>}} `types` is every type the tree actually contains, so a qualifier naming no real type can be told apart from a missing target. | Builds the link-resolution tables for a content tree. |
|
|
675
|
+
| `convertWikilinks` | `function convertWikilinks(markdown,` | {{markdown: string, unresolved: Array<{link: string, target: string, offset: number, reason: string, packages?: string[], anchor?: string}>}} Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all three resolvers share — `ambiguous` carries the claiming `packages` and `unknown-anchor` the section it named. | Rewrites every wikilink in a markdown body as a Foundry UUID enricher. |
|
|
676
|
+
|
|
677
|
+
### `engine.wikilinkSyntax`
|
|
678
|
+
|
|
679
|
+
What a `[[…]]` **is**, before anything decides where it points. One authored link compiles to two different addresses — a Foundry `@UUID` enricher for the packs, a URL for the web — and those two destinations are the _only_ thing that legitimately differs. The syntax is the author's, and it is the same syntax whichever build is reading it.
|
|
680
|
+
|
|
681
|
+
| Export | Signature | Returns | Use it when |
|
|
682
|
+
| -------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
683
|
+
| `WIKILINK` | `const WIKILINK` | — | A wikilink, as authored. |
|
|
684
|
+
| `parseWikilink` | `function parseWikilink(rawInner)` | {ParsedWikilink} The parts, each trimmed. | Split a wikilink's interior into its parts. |
|
|
685
|
+
| `authoredLabel` | `authoredLabel({ display })` | {string\|null} The label, or `null` when there is none to show. | The label an author actually supplied, or `null` when they supplied none. |
|
|
686
|
+
| `unlabelledLinkMessage` | `function unlabelledLinkMessage(target)` | {string} The message, unpunctuated at the end as a finding is. | What an author writing an unlabelled link is told, in one place. |
|
|
687
|
+
| `LINK_FINDING_REASONS` | `const LINK_FINDING_REASONS` | — | Every way a link can fail, named once for all three resolvers. |
|
|
688
|
+
| `unresolvedAddressMessage` | `function unresolvedAddressMessage(target)` | {string} The message, unpunctuated at the end as a finding is. | What an author writing an address that resolves to nothing is told. |
|
|
689
|
+
| `ambiguousAddressMessage` | `function ambiguousAddressMessage(target, packages` | {string} The message. | What an author writing a short address more than one package publishes is told. |
|
|
690
|
+
| `linkFindingMessage` | `linkFindingMessage({ reason, target, packages, anchor })` | {string} The message. | The message for one link finding, whichever resolver found it. |
|
|
691
|
+
| `isSamePage` | `isSamePage({ target, anchor })` | {boolean} True when the link is same-page. | Whether a parsed link addresses a section of the page it is written on. |
|
|
692
|
+
|
|
693
|
+
### `engine.siteIndex`
|
|
694
|
+
|
|
695
|
+
**The address index a site build resolves its wikilinks against.** Every consumer that publishes a content tree as a website has to answer the same question — given `[[Something]]`, which page? — and every one of them answered it with its own copy of the same 150 lines. `sohl`'s and `sohl-thalorna`'s site builds still share 147 identical lines of it, comments and indentation aside. This is that shared half, lifted out whole.
|
|
696
|
+
|
|
697
|
+
| Export | Signature | Returns | Use it when |
|
|
698
|
+
| ---------------- | ---------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
699
|
+
| `buildSiteIndex` | `function buildSiteIndex(entries,` | {SiteIndex} The index, and what could not be addressed unambiguously. | Build the address index a site's wikilink resolver reads. |
|
|
700
|
+
| `wikiContext` | `function wikiContext(built,` | {object} The resolver context. | The per-page context a wikilink resolver takes. |
|
|
701
|
+
|
|
702
|
+
### `engine.pdfToc`
|
|
703
|
+
|
|
704
|
+
The document tree a PDF is built from, and the plan it resolves to (#316). The packs and the website both render the _whole_ content tree: every note becomes a document and a page, and the three surfaces agreeing about what the content is, is the point. **A book is not that.** It is a selection — a declared structure whose leaves pick notes out of the corpus by a `WHERE` clause, interleaved with prose that may not live in the content tree at all.
|
|
705
|
+
|
|
706
|
+
| Export | Signature | Returns | Use it when |
|
|
707
|
+
| ------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
708
|
+
| `PRESENTATION_KEYS` | `const PRESENTATION_KEYS` | — | Presentation a node may declare, and that its descendants inherit. |
|
|
709
|
+
| `parseDocumentTree` | `function parseDocumentTree(raw,` | {{nodes: object[], findings: object[]}} The flattened tree and what was wrong with it. | Validate the raw tree and flatten it into nodes. |
|
|
710
|
+
| `runTreeFilters` | `async function runTreeFilters(nodes, db,` | {Promise<{selections: Map<string, object[]>, findings: object[]}>} | Run every filter, and report the ones that would not run. |
|
|
711
|
+
| `planDocument` | `function planDocument(nodes,` | {{entries: object[], links: Map<string, string>, stats: object}} The plan, the address→anchor map inbound wikilinks resolve through, and what the selection came to. | Resolve the flattened tree into the document plan. |
|
|
712
|
+
|
|
713
|
+
### `engine.pdfRender`
|
|
714
|
+
|
|
715
|
+
A note's markdown, and a document plan, rendered as Typst source. **This module emits text and reads nothing.** It takes markdown and a plan and returns a `.typ` document; the filesystem, the note bodies and the compiler that turns the result into a PDF all live in {@link module:engine/pdf-build}. That split is what lets the outline, the table of contents, every anchor and every link destination be asserted in a unit test with no renderer installed — which is most of what a book has to get right, and all of what a test can check without eyes.
|
|
716
|
+
|
|
717
|
+
| Export | Signature | Returns | Use it when |
|
|
718
|
+
| ----------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
719
|
+
| `escapeTypst` | `function escapeTypst(text)` | {string} The same text, inert. | Escape literal text for Typst markup. |
|
|
720
|
+
| `escapeTypstString` | `function escapeTypstString(text)` | {string} The same value, quotable. | Escape a string going inside Typst string quotes, as a `#link` URL does. |
|
|
721
|
+
| `labelFor` | `function labelFor(anchor)` | {string} A Typst label name. | A Typst label, from a plan anchor. |
|
|
722
|
+
| `createParser` | `function createParser(registry)` | {object} A markdown-it instance. | A markdown-it configured to parse, not to render. |
|
|
723
|
+
| `markdownToTypst` | `function markdownToTypst(markdown, opts` | {string} Typst markup. | Render markdown as Typst content. |
|
|
724
|
+
| `renderBook` | `renderBook({ plan, bodies, title, subtitle, front, fonts, version })` | {string} A complete `.typ` document. | The whole book, as one Typst document. |
|
|
725
|
+
| `resolveDanglingLabels` | `function resolveDanglingLabels(source, findings` | {string} The same document, with no reference left dangling. | Point every internal link at a label the document actually declares. |
|
|
726
|
+
| `iconNamesIn` | `function iconNamesIn(markdown)` | {string[]} The names, in order of appearance, with repeats. | Every icon name a body uses, so a build can resolve them once. |
|
|
727
|
+
|
|
728
|
+
### `engine.pdfFonts`
|
|
729
|
+
|
|
730
|
+
Which glyph an icon name resolves to, read from the font that carries it. {@link module:engine/content-icons} states an icon's family and its name and **deliberately holds no codepoints**: writing them out by hand would be a second copy of a table the font already owns, wrong the first time the icon set renumbers anything, and wrong silently. So the renderer resolves a name against the file it is going to embed, which is this module.
|
|
731
|
+
|
|
732
|
+
| Export | Signature | Returns | Use it when |
|
|
733
|
+
| ------------------- | ------------------------------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
734
|
+
| `glyphTable` | `function glyphTable(file)` | {Map<string, number>} Name → codepoint. | Every glyph name a font carries, with the codepoint that reaches it. |
|
|
735
|
+
| `familyName` | `function familyName(file)` | {string} The family name, or "" when the table cannot be read. | The family name a font file announces, for Typst's `text(font: …)`. |
|
|
736
|
+
| `resolveIconGlyphs` | `function resolveIconGlyphs(registry, iconFonts` | {Map<string, {font: string, codepoint: number}>} Icon name → glyph. | Resolve every icon in a registry against the fonts a consumer named. |
|
|
737
|
+
|
|
738
|
+
### `engine.pdfBuild`
|
|
739
|
+
|
|
740
|
+
The content tree, built into a book. The I/O half of the PDF surface: it reads the configuration, the document tree and the notes, drives the passes the site build already owns, hands the result to {@link module:engine/pdf-render} and runs Typst over what comes back. Everything about _what the book says_ is decided in the pure half; this module is where the filesystem and the compiler live.
|
|
741
|
+
|
|
742
|
+
| Export | Signature | Returns | Use it when |
|
|
743
|
+
| -------------- | --------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
744
|
+
| `pdfFileName` | `function pdfFileName(artifact, version)` | {string} `<artifact>-<version>.pdf`, or `<artifact>.pdf` unversioned. | The file name a downloaded book identifies itself by. |
|
|
745
|
+
| `buildPdf` | `async buildPdf({ config, out, version, compile })` | {Promise<object>} `{ built, reason, findings, typ, pdf, stats }`. | Build the book. |
|
|
746
|
+
| `compileTypst` | `function compileTypst(typPath, pdfPath, pdf` | {{ok: boolean, message: string}} What happened. | Run Typst over the emitted source. |
|
|
747
|
+
|
|
748
|
+
### `engine.baseCompiler`
|
|
749
|
+
|
|
750
|
+
`BasePackCompiler` — the one compile loop every pack pass runs. Walking the content tree, rejecting what this build does not own, expanding generated tables, converting wikilinks, writing the JSON and counting what failed are the same in every pass. They were written out once per pass — three times when this was filed, five by the time it landed — so a fix to any of them had to be made everywhere, and the passes drifted apart in exactly the places nobody was comparing.
|
|
751
|
+
|
|
752
|
+
| Export | Signature | Returns | Use it when |
|
|
753
|
+
| ------------------ | ------------------------ | ------- | ------------------------------------------------------------------------- |
|
|
754
|
+
| `BasePackCompiler` | `class BasePackCompiler` | — | The shared walk → filter → expand → convert → build → write → count loop. |
|
|
755
|
+
|
|
756
|
+
### `engine.journals`
|
|
757
|
+
|
|
758
|
+
Journals pack compiler — produces JSON pack files for the "journals" Foundry compendium from markdown notes in the `assets/content/` tree. The content root (`contentBase`) is walked recursively; any `.md` file whose frontmatter declares either `type: doc` or a **doc-carrying type** ({@link sohl.utils.packs.docEntryTypes} — every item type, plus `macro`) is compiled into one JournalEntry document. Each note's body is split on top-level H1 headings; the optional content before the first H1 becomes a lead page, and each subsequent H1 starts a new page named after its heading text. All page bodies are rendered to HTML.
|
|
759
|
+
|
|
760
|
+
| Export | Signature | Returns | Use it when |
|
|
761
|
+
| --------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
762
|
+
| `splitPages` | `function splitPages(body, leadName` | — | Splits a markdown body into pages by top-level H1 headings. |
|
|
763
|
+
| `assertUniquePages` | `function assertUniquePages(rawPages, noteName)` | — | Two pages in one note that would derive the same id, which the LevelDB packer reports only as an opaque duplicate-key collision. |
|
|
764
|
+
| `assertUniqueAnchors` | `function assertUniqueAnchors(rawPages, noteName)` | — | The anchor half of {@link assertUniquePages}, under its former name. |
|
|
765
|
+
| `journalPageId` | `function journalPageId(entryId, page)` | {string} A 16-character Foundry id. | The id of one page within its entry. |
|
|
766
|
+
| `buildPages` | `function buildPages(rawPages, entryId, noteName)` | {Array<{_id: string, name: string, type: string, title: {show: boolean, level: number}, text: {format: number, content: string}, _key: string}>} The page documents, in order. | Compile split pages into JournalEntryPage documents. |
|
|
767
|
+
| `buildJournalEntry` | `buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats })` | {object} The JournalEntry document, keyed for the pack. | Assemble one JournalEntry document from a note's converted markdown. |
|
|
768
|
+
| `Journals` | `class Journals extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "journals" pack compiler |
|
|
769
|
+
|
|
770
|
+
### `engine.macros`
|
|
771
|
+
|
|
772
|
+
Macros pack compiler — produces JSON pack files for the "macros" Foundry compendium from markdown notes in the `assets/content/` tree. A `type: macro` note compiles into **two** documents, and this module writes only the first of them:
|
|
773
|
+
|
|
774
|
+
| Export | Signature | Returns | Use it when |
|
|
775
|
+
| --------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
776
|
+
| `MACRO_SCRIPT_ANCHOR` | `const MACRO_SCRIPT_ANCHOR` | — | The anchor the executable script lives under: `# Script {#script}`. |
|
|
777
|
+
| `MACRO_TYPES` | `const MACRO_TYPES` | — | The Foundry macro types (`CONST.MACRO_TYPES`). |
|
|
778
|
+
| `MACRO_SCOPES` | `const MACRO_SCOPES` | — | The Foundry macro scopes (`CONST.MACRO_SCOPES`), in schema order. |
|
|
779
|
+
| `DEFAULT_MACRO_IMG` | `const DEFAULT_MACRO_IMG` | — | Foundry's own default macro artwork, used when a note authors no `img`. |
|
|
780
|
+
| `extractJsFence` | `function extractJsFence(markdown)` | {string\|null} The fence's contents, with no trailing newline, or `null` when the block holds no tagged JS fence. | The body of the first **language-tagged** JavaScript fence in a markdown block, verbatim. |
|
|
781
|
+
| `macroCommand` | `function macroCommand(body, name)` | {string} The macro's command. | The `command` a macro note compiles to: the first tagged JS fence on its `{#script}` page. |
|
|
782
|
+
| `resolveMacroType` | `function resolveMacroType(fm, label)` | {"script"} The macro type. | The **Foundry** macro type a note compiles to — not the note's `type:`, which stays `macro` because that is what routes it to this pack. |
|
|
783
|
+
| `resolveMacroScope` | `function resolveMacroScope(fm, label)` | {string} One of {@link MACRO_SCOPES}. | The Foundry macro scope a note compiles to. |
|
|
784
|
+
| `buildMacroEntry` | `function buildMacroEntry(fm,` | {MacroDocument} The Macro document. | The compendium envelope for one Macro. |
|
|
785
|
+
| `Macros` | `class Macros extends BasePackCompiler` | — | Macros pack compiler. |
|
|
786
|
+
|
|
787
|
+
### `engine.mapNotes`
|
|
788
|
+
|
|
789
|
+
**Map notes** — the markdown → Foundry `Scene` translation. A map note carries an _essence_: a curated, hand-owned subset of what a Scene record holds, exactly as a weapon note carries a weapon's essence rather than an Item's schema. Everything a Scene needs and nobody should have to author — the canvas defaults, the embedded `Level`, every derived region field — is synthesised here.
|
|
790
|
+
|
|
791
|
+
| Export | Signature | Returns | Use it when |
|
|
792
|
+
| ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
793
|
+
| `isMapType` | `function isMapType(type)` | {boolean} True for a map type. | Whether a content note's type compiles into a Scene. |
|
|
794
|
+
| `MAP_SUBTYPE_PROFILES` | `const MAP_SUBTYPE_PROFILES` | — | Per-subtype canvas defaults, emitted **explicitly** on every scene. |
|
|
795
|
+
| `mapProfile` | `function mapProfile(subType)` | {object} The profile from {@link MAP_SUBTYPE_PROFILES}. | The canvas profile for a map subType. |
|
|
796
|
+
| `DEFAULT_LEVEL_ID` | `const DEFAULT_LEVEL_ID` | — | Foundry's own id for the level a scene is created with (`Scene.metadata.defaultLevelId`). |
|
|
797
|
+
| `regionDocId` | `function regionDocId(sceneId, key, pinned)` | {string} A 16-character Foundry id. | The id of one region within its scene. |
|
|
798
|
+
| `behaviorDocId` | `function behaviorDocId(regionId, key, pinned)` | {string} A 16-character Foundry id. | The id of one behaviour within its region. |
|
|
799
|
+
| `regionColor` | `function regionColor(key)` | {string} A CSS hex colour. | A region's highlight colour, derived from its key. |
|
|
800
|
+
| `assertPixelGeometry` | `function assertPixelGeometry(coords, geom)` | — | Reject geometry authored in grid squares where pixels belong. |
|
|
801
|
+
| `assertGridLocation` | `function assertGridLocation(at, geom)` | — | Reject a map pin authored in pixels where grid squares belong. |
|
|
802
|
+
| `wallRestrictions` | `function wallRestrictions(spec, label)` | {{move: number, sight: number, light: number, sound: number}} The Wall restriction fields. | Compile a wall's `blocks:` / `limits:` lists into Foundry's four numeric restriction fields. |
|
|
803
|
+
| `buildShape` | `function buildShape(spec, geom)` | {object} The Foundry shape record. | Compile one authored shape into a Foundry shape record. |
|
|
804
|
+
| `REGION_BEHAVIOR_TYPES` | `const REGION_BEHAVIOR_TYPES` | — | The behaviour types a map note may carry (v1). |
|
|
805
|
+
| `BANNED_REGION_BEHAVIOR_TYPES` | `const BANNED_REGION_BEHAVIOR_TYPES` | — | Behaviour types a map note may **never** carry, and why. |
|
|
806
|
+
| `buildScene` | `function buildScene(fm, ctx)` | {object} The Scene document, keyed for the pack. | Compile a map note into a Scene document, embedded documents and all. |
|
|
807
|
+
| `buildLevel` | `function buildLevel(sohl, sceneId, img` | {object} The Level document, keyed for the pack. | Synthesise the scene's single embedded Level from `img:` / `overlay:`. |
|
|
808
|
+
| `buildWalls` | `function buildWalls(sohl, geom, ctx)` | {object[]} The Wall documents. | Compile the `walls:` and `doors:` blocks into Wall documents. |
|
|
809
|
+
| `buildLights` | `function buildLights(sohl, geom, ctx)` | {object[]} The AmbientLight documents. | Compile the `lights:` block into AmbientLight documents. |
|
|
810
|
+
| `buildTiles` | `function buildTiles(sohl, geom, ctx)` | {object[]} The Tile documents. | Compile the `tiles:` block into Tile documents. |
|
|
811
|
+
| `buildSounds` | `function buildSounds(sohl, geom, ctx)` | {object[]} The AmbientSound documents. | Compile the `sounds:` block into AmbientSound documents. |
|
|
812
|
+
| `buildLocations` | `function buildLocations(sohl, geom, ctx)` | {object[]} The Note documents. | Compile the `locations:` block into Note documents — the map pins. |
|
|
813
|
+
| `buildRegions` | `function buildRegions(sohl, geom, ctx)` | {object[]} The Region documents. | Compile the `regions:` block into Region documents with their behaviours. |
|
|
814
|
+
|
|
815
|
+
### `engine.scenes`
|
|
816
|
+
|
|
817
|
+
Scenes pack compiler — map notes in `assets/content/` → Foundry `Scene` documents, and the `Adventure` bundles that make their references resolve. The translation itself lives in the framework-free `map-notes.mjs`; this module is the pass that walks the tree, resolves what one note says about another, and writes the JSON the compendium CLI compiles.
|
|
818
|
+
|
|
819
|
+
| Export | Signature | Returns | Use it when |
|
|
820
|
+
| ------------------------- | -------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
821
|
+
| `collectKnownActionNames` | `function collectKnownActionNames(repoRoot)` | {Set<string>} The known action names. | Every SoHL action name this build knows about, for the `action:` warning on a region trigger. |
|
|
822
|
+
| `Scenes` | `class Scenes extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "scenes" pack compiler; walks the tree, resolves cross-scene references, and writes the JSON the compendium CLI compiles |
|
|
823
|
+
|
|
824
|
+
### `engine.bundleNotes`
|
|
825
|
+
|
|
826
|
+
Bundles, as notes — the Foundry `Adventure` a `type: bundle` note compiles into. An `Adventure` is badly named, and the name misled the first design: it is not a story. `Adventure.contentFields` maps each `SetField` on the schema to a document class, and importing one partitions its members by whether the world's collection already holds that `_id`, then creates or updates each. Afterwards the documents live independently and the Adventure has no further role. **It is an installer** — a set of document _copies_ packaged for one-shot import.
|
|
827
|
+
|
|
828
|
+
| Export | Signature | Returns | Use it when |
|
|
829
|
+
| ------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
830
|
+
| `BUNDLE_TYPE` | `const BUNDLE_TYPE` | — | The note type a bundle is authored as. |
|
|
831
|
+
| `ADVENTURE_CONTENT_FIELD` | `const ADVENTURE_CONTENT_FIELD` | — | Foundry's `Adventure.contentFields`, keyed by the document class each holds. |
|
|
832
|
+
| `CONTENTS_FIELD` | `const CONTENTS_FIELD` | — | The `data:` key a bundle lists its members under. |
|
|
833
|
+
| `stripAdventureKeys` | `function stripAdventureKeys(value)` | {*} The same shape with every `_key` removed. | Strip the LevelDB keys from a document tree. |
|
|
834
|
+
| `bareAddress` | `function bareAddress(value)` | {string\|null} The bare address, or `null` for a blank entry. | An authored address with any wikilink brackets and label stripped. |
|
|
835
|
+
| `bundleContents` | `function bundleContents(fm)` | {string[]} The bare addresses. | The addresses a bundle note names, in the order it names them. |
|
|
836
|
+
| `missingMemberVerdict` | `function missingMemberVerdict(packSystem)` | {"omit"\|"fail"} What to do about a member the sources lack. | What a pass should do about a member its sources do not hold. |
|
|
837
|
+
| `buildAdventure` | `buildAdventure({ id, name, img, description, caption, folder, flags, stats, contents })` | {object} The Adventure document, keyed for the pack. | Assemble one `Adventure` from a set of already-compiled documents. |
|
|
838
|
+
|
|
839
|
+
### `engine.bundles`
|
|
840
|
+
|
|
841
|
+
Adventure pack compiler — `type: bundle` notes → Foundry `Adventure` documents. The specification and the vocabulary leave the type declared and uncompiled: authoring one said so, in as many words. This is the pass, and the two decisions it records are settled here.
|
|
842
|
+
|
|
843
|
+
| Export | Signature | Returns | Use it when |
|
|
844
|
+
| ------------------- | ---------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
845
|
+
| `loadBundleSources` | `function loadBundleSources(sourceDirs)` | {Map<string, object>} The compiled documents. | Load every compiled document a bundle may hold, keyed `<docType>/<id>`. |
|
|
846
|
+
| `Bundles` | `class Bundles extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the pass a `type: bundle` note compiles through, assembling the `Adventure` from `loadBundleSources`' already-compiled documents |
|
|
847
|
+
|
|
848
|
+
### `engine.sceneLevels`
|
|
849
|
+
|
|
850
|
+
**Scene ↔ Level integrity** for a compiled compendium pack. A v14 Scene keeps its map image on an embedded `Level`, and a compiled pack stores the two in _separate_ LevelDB keys: the Scene at `!scenes!<id>` holding `levels` as an array of ids, and each Level at `!scenes.levels!<sceneId>.<levelId>`. Nothing in Foundry ties them together on read. If a Level record is missing, `EmbeddedCollectionField#expandEmbedded` merely warns
|
|
851
|
+
|
|
852
|
+
| Export | Signature | Returns | Use it when |
|
|
853
|
+
| ----------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
854
|
+
| `compendiumCliVersion` | `function compendiumCliVersion()` | {string \| undefined} The resolved version, or `undefined` when the package cannot be read (a pruned install, an exports-restricted copy). | The version of `@foundryvtt/foundryvtt-cli` this build actually resolves. |
|
|
855
|
+
| `checkSceneLevels` | `function checkSceneLevels(records,` | {string[]} One human-readable problem per violation, empty when the pack is sound. | Every way a compiled pack can ship a Scene that has lost its Level. |
|
|
856
|
+
| `verifyPackSceneLevels` | `async function verifyPackSceneLevels(packDir)` | {Promise<string[]>} The problems found, empty when the pack is sound. | Read a compiled pack back off disk and check it. |
|
|
857
|
+
|
|
858
|
+
### `engine.generate`
|
|
859
|
+
|
|
860
|
+
Pack JSON generation — in-repo Markdown → per-entry JSON (build-only). Reads the authoritative content tree at the configured content root and compiles each pack's entries to per-entry JSON under its build directory (`build/packs-json/<pack>/` in this repository). The JSON is a disposable build intermediate consumed by `build:compiledb` (which turns it into the shipped LevelDB packs) — it is never committed.
|
|
861
|
+
|
|
862
|
+
| Export | Signature | Returns | Use it when |
|
|
863
|
+
| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
864
|
+
| `compilerFor` | `function compilerFor(docType, system` | {Function\|undefined} The compiler class, or `undefined` for a document type nothing here compiles — which {@link generatePack} reports rather than defaulting past. | The compiler class a pack of one document type and one system gets. |
|
|
865
|
+
| `emittedArtFor` | `function emittedArtFor(type)` | {{document: string\|null, art: readonly string[]}\|null} What the type compiles into and the art it carries there, or `null` where no claim can be made — a retired type, which is reported as retired instead. | The art fields a note of one content type reaches its document through, and the document it reaches. |
|
|
866
|
+
| `packJsonDir` | `const packJsonDir` | {string} The pack's JSON directory. | Root of the build-only JSON tree for one pack. |
|
|
867
|
+
| `itemPackJsonDirs` | `function itemPackJsonDirs(config` | {string[]} Each Item pack's JSON directory. | The generated JSON of **every** configured Item pack — what the actors pass reads its predefined items from. |
|
|
868
|
+
| `bundleSourceJsonDirs` | `function bundleSourceJsonDirs(config` | {Record<string, string[]>} Each readable pack's JSON directory, by the Foundry document type it holds. | The compiled JSON a bundle may hold copies of, by document type. |
|
|
869
|
+
| `orderPassesByDependency` | `function orderPassesByDependency(packs)` | {object[]} A new list, in compile order. | The passes to run, ordered so that each one follows the output it reads. |
|
|
870
|
+
| `unsatisfiedPassDependencies` | `function unsatisfiedPassDependencies(running, config)` | {string[]} One message per unsatisfiable dependency. | The dependencies this run cannot satisfy by ordering, because the pass that would produce them is not in it. |
|
|
871
|
+
| `emptyPassErrors` | `function emptyPassErrors(passes)` | {string[]} One message per pass that must not have been empty. | The passes that compiled nothing when they were expected to compile something — a build failure, not a quiet no-op. |
|
|
872
|
+
| `generatePacksJson` | `async generatePacksJson({ only, config })` | {Promise<number>} Total error count across the generated packs. | Generate the build-only JSON for every pack (or one, when `only` is given). |
|
|
873
|
+
|
|
874
|
+
### `engine.compendiums`
|
|
875
|
+
|
|
876
|
+
Compendium pack library — compile / unpack / clean LevelDB packs. Wraps `@foundryvtt/foundryvtt-cli` over the packs a consuming repository declares: - {@link compilePacks}: generates each pack's per-entry JSON from the `assets/content/` Markdown into `build/packs-json/<name>/` (via generate.mjs), then builds LevelDB from it; no committed JSON, no vault. - {@link unpackPacks}: extracts a compiled pack back to per-entry JSON, rebuilding folder paths. - {@link cleanPacks}: normalizes/strips extracted JSON.
|
|
877
|
+
|
|
878
|
+
| Export | Signature | Returns | Use it when |
|
|
879
|
+
| -------------- | --------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
880
|
+
| `compilePacks` | `async compilePacks({ config, sourcePacks, stageDest, packName })` | — | Generates each pack's per-entry JSON from `assets/content/` into `build/packs-json/<name>/`, then builds the LevelDB output from it. |
|
|
881
|
+
| `cleanPacks` | `async cleanPacks({ config, packDest, packName, entryName })` | — | Cleans and formats source JSON files, removing unnecessary permissions and flags and adding the proper spacing. |
|
|
882
|
+
| `unpackPacks` | `async unpackPacks({ packs, config, stageDest, packDest, packName })` | — | Extracts compiled LevelDB packs back to per-entry JSON, rebuilding the folder hierarchy as directories. |
|
|
883
|
+
|
|
884
|
+
### `engine.regionEvents`
|
|
885
|
+
|
|
886
|
+
The curated Foundry region-event vocabulary, as plain data. Deliberately **plain ESM** — no TypeScript, no `@src` aliases, no Foundry — for the same reason `../sohl/default-item-art.mjs` is: the map-note pack compiler runs under bare `node`, outside the bundler that resolves `@src` and strips types, and it must reject an authored region event that the runtime would silently drop. One list here is what keeps the build-time lint and the runtime bridge from drifting apart.
|
|
887
|
+
|
|
888
|
+
| Export | Signature | Returns | Use it when |
|
|
889
|
+
| ------------------------- | ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
890
|
+
| `REGION_EVENT_TO_TRIGGER` | `const REGION_EVENT_TO_TRIGGER` | — | The curated Foundry region-event → SoHL trigger-name map. |
|
|
891
|
+
| `CURATED_REGION_EVENTS` | `const CURATED_REGION_EVENTS` | — | The Foundry region-event names SoHL forwards (the keys of the map). |
|
|
892
|
+
| `EXCLUDED_REGION_EVENTS` | `const EXCLUDED_REGION_EVENTS` | — | Region events SoHL deliberately does **not** forward: the continuous (`tokenMove*`), view-dependent (`tokenAnimate*`) and lifecycle (`behavior*`, `regionBoundary`) streams. |
|
|
893
|
+
|
|
894
|
+
## `./sohl`
|
|
895
|
+
|
|
896
|
+
The SoHL-specific half of the toolchain: the knowledge of the Song of Heroic Lands data model that a generic content module must never receive. The item-type registry and its builders, the Item and Actor compilers, the default-art map, and the affiliation standings live here — nothing in `@heroiclands/package-build/engine` exports any of it, so an adventure module that builds journals, macros and scenes never receives `buildWeaponGear`. A consuming repository hands its own registry to the engine as configuration (`itemBuilders` in `package-build.config.yaml`), which is how the engine composes one doc-carrying-type set without holding any package's data model.
|
|
897
|
+
|
|
898
|
+
Every module beneath `./sohl` is also reachable as its own entry point, e.g. `@heroiclands/package-build/sohl/being-info`.
|
|
899
|
+
|
|
900
|
+
```js
|
|
901
|
+
import { isBeing, GEAR_TYPE_TO_KEY, deriveBeingInfo } from "@heroiclands/package-build/sohl";
|
|
902
|
+
|
|
903
|
+
console.log(isBeing({ type: "being" }));
|
|
904
|
+
// -> true
|
|
905
|
+
console.log(GEAR_TYPE_TO_KEY.weapongear);
|
|
906
|
+
// -> "weapons"
|
|
907
|
+
console.log(deriveBeingInfo({ items: [] }, new Map()));
|
|
908
|
+
// -> { items: [] }
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### `sohl.itemBuilders`
|
|
912
|
+
|
|
913
|
+
The item-type registry: every content type that compiles into a Foundry Item, keyed to the builder that produces its `system` block. The registry's keys are generated from `item-fields.mjs`'s field declarations rather than written by hand, so the compilable-type whitelist and the builder table cannot drift apart the way they once could.
|
|
914
|
+
|
|
915
|
+
| Export | Signature | Returns | Use it when |
|
|
916
|
+
| --------------- | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
917
|
+
| `ITEM_BUILDERS` | `const ITEM_BUILDERS` | — | reading every SoHL item type paired with its `system`-block builder, its default art, and its frontmatter fields — handed to the engine as `itemBuilders: sohl` in configuration |
|
|
918
|
+
|
|
919
|
+
### `sohl.documentSubtypes`
|
|
920
|
+
|
|
921
|
+
SoHL's note-type → document-subtype map: which Foundry document, and which subtype of it, a note of each content type compiles into. Every row is written out explicitly, identity rows included, so the note vocabulary and the document vocabulary stay two separately stated facts rather than one generating the other.
|
|
922
|
+
|
|
923
|
+
| Export | Signature | Returns | Use it when |
|
|
924
|
+
| ------------------------ | ------------------------------ | ------- | ------------------------------------------------------------------------------- |
|
|
925
|
+
| `SOHL_DOCUMENT_SUBTYPES` | `const SOHL_DOCUMENT_SUBTYPES` | — | looking up which Foundry document and subtype a SoHL content type compiles into |
|
|
926
|
+
|
|
927
|
+
### `sohl.items`
|
|
928
|
+
|
|
929
|
+
SoHL's Item pass — the parts of compiling a note into a SoHL Item that are facts about SoHL rather than about the note format: the note-type → document-subtype map, and the `commonSystem` fields (`shortcode`, `templatePriority`, `actionDefs`, `notes`, `docHtml`) every SoHL item carries regardless of type. Everything else lives in the engine's generic item compiler.
|
|
930
|
+
|
|
931
|
+
| Export | Signature | Returns | Use it when |
|
|
932
|
+
| ------- | ------------- | ------- | -------------------------------------------------------------------------------------------------------- |
|
|
933
|
+
| `Items` | `class Items` | — | not called directly — this compiler class is imported and driven by `engine/generate.mjs` during a build |
|
|
934
|
+
|
|
935
|
+
### `sohl.actors`
|
|
936
|
+
|
|
937
|
+
SoHL's Actor pass — what a SoHL `being` document holds and nothing else: the body structure and its movement profiles, the attributes-and-items frontmatter that becomes embedded documents, the opening mastery level a skill is baked with, and the `system` block itself. The shared machinery (predefined-item catalogue, reference translation, embedding, anchored prose sections) lives in the engine's generic actor compiler.
|
|
938
|
+
|
|
939
|
+
| Export | Signature | Returns | Use it when |
|
|
940
|
+
| -------- | -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
941
|
+
| `Actors` | `class Actors` | — | not called directly — imported and driven by `engine/generate.mjs`; must run after the items passes, since it reads their generated JSON trees |
|
|
942
|
+
|
|
943
|
+
### `sohl.kbPasses`
|
|
944
|
+
|
|
945
|
+
The `sohl` knowledgebase's own body passes: two rewrites driven by a TypeDoc symbol map and a repository layout only this package has, named from `site.passOptions` the same way an asset transform is named from configuration. Neither rewrite ever fails a build — an unresolved `{@link}` degrades to a code span, and a relative link outside the documentation tree becomes a GitHub blob URL — but building the bundle from a misconfigured `symbolMap` fails loudly before any page renders.
|
|
946
|
+
|
|
947
|
+
| Export | Signature | Returns | Use it when |
|
|
948
|
+
| ------------------ | ----------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
949
|
+
| `resolveApiLinks` | `resolveApiLinks(body, symbols, apiBase)` | `string` — the body with every tag resolved | resolving inline TypeDoc `{@link}` / `{@linkcode}` / `{@linkplain}` tags in a markdown body against the API symbol map |
|
|
950
|
+
| `rewriteRepoLinks` | `rewriteRepoLinks(body, docRel, options)` | `string` — the body with every relative link rewritten | rewriting a developer doc's repository-relative links so they resolve on the published site (documentation-tree links become routes, everything else becomes a GitHub blob URL) |
|
|
951
|
+
| `sohlKbPass` | `sohlKbPass(options)` | `{beforeLinks: Function, afterLinks: Function}` | building the `sohl` knowledgebase pass bundle the site renderer calls around wikilink resolution |
|
|
952
|
+
|
|
953
|
+
### Flat exports (not under a namespace)
|
|
954
|
+
|
|
955
|
+
| Export | Signature | Returns | Use it when |
|
|
956
|
+
| ----------------------- | ------------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
957
|
+
| `DEFAULT_ITEM_ART` | `const DEFAULT_ITEM_ART` | — | reading the full map of default item artwork keyed by SoHL Item document subtype — the single source both the compendium builder and the runtime `SohlItem.getDefaultArtwork` read |
|
|
958
|
+
| `defaultItemArt` | `defaultItemArt(type)` | `string` — the default image path | getting the default art path for a document subtype, throwing (fail-fast) rather than defaulting silently when the type is unknown |
|
|
959
|
+
| `AFFILIATION_STANDINGS` | `const AFFILIATION_STANDINGS` | — | reading the closed set of stances an authored `relation` map may use between two affiliations |
|
|
960
|
+
| `BEING_TYPE` | `const BEING_TYPE` | — | the one note `type` value (`"being"`) whose pages carry a being info block; the retired `character`/`creature` spellings are deliberately not accepted here |
|
|
961
|
+
| `GEAR_TYPE_TO_KEY` | `const GEAR_TYPE_TO_KEY` | — | mapping a gear note type (`weapongear`, `armorgear`, …) to the sidebar heading it displays under (`weapons`, `armor`, …) |
|
|
962
|
+
| `isBeing` | `isBeing(fm)` | `boolean` | checking whether a note's frontmatter describes a being, using the one shared definition instead of a per-repository copy |
|
|
963
|
+
| `deriveBeingInfo` | `deriveBeingInfo(sohl, index)` | `object\|null\|undefined` | deriving a being's info-block fields (`skills`, `gear`, `spells`, `talents`) from its raw embedded `sohl.items[]`, resolved against a content index; authored values always win over derived ones |
|
|
964
|
+
|
|
965
|
+
## `./hm3`
|
|
966
|
+
|
|
967
|
+
The HM3-specific half of the toolchain: the knowledge of the HârnMaster 3 data model that a generic content module must never receive. HM3's item-type registry and its builders, its note-type → document-subtype map, its two compilers and its default-art map live here; it imports nothing from `sohl/` and `sohl/` imports nothing from it — the only thing the two systems share is the engine between them. A repository shipping content for both names both registries (`itemBuilders: [sohl, hm3]`) and declares one pack per system per document type.
|
|
968
|
+
|
|
969
|
+
Every module beneath `./hm3` is also reachable as its own entry point, e.g. `@heroiclands/package-build/hm3/item-fields`.
|
|
970
|
+
|
|
971
|
+
```js
|
|
972
|
+
import { hm3DefaultItemArt, documentSubtypes } from "@heroiclands/package-build/hm3";
|
|
973
|
+
|
|
974
|
+
console.log(hm3DefaultItemArt("weapongear"));
|
|
975
|
+
// -> "systems/hm3/images/icons/svg/sword.svg"
|
|
976
|
+
console.log(documentSubtypes.HM3_TYPE_KEY);
|
|
977
|
+
// -> "type"
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
### `hm3.itemBuilders`
|
|
981
|
+
|
|
982
|
+
The item-type registry: every content type that compiles into an HM3 Foundry Item, keyed to the builder that produces its `system` block, generated from `item-fields.mjs`'s declarations the same way `sohl.itemBuilders` is. A type both systems' registries declare throws rather than resolving unless the caller says which system is asking.
|
|
983
|
+
|
|
984
|
+
| Export | Signature | Returns | Use it when |
|
|
985
|
+
| ------------------- | ------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
|
|
986
|
+
| `HM3_ITEM_BUILDERS` | `const HM3_ITEM_BUILDERS` | — | reading every HM3 item type paired with its `system`-block builder, its default art, and its frontmatter fields |
|
|
987
|
+
|
|
988
|
+
### `hm3.itemFields`
|
|
989
|
+
|
|
990
|
+
The `hm3:` frontmatter vocabulary of every HM3 item type, keyed by **note** type. Deliberately shorter than SoHL's field table: where the content format's mapping tables state no destination for HM3, this file declares nothing rather than inventing a plausible one, since a guessed field compiles clean and is silently discarded by Foundry at load.
|
|
991
|
+
|
|
992
|
+
| Export | Signature | Returns | Use it when |
|
|
993
|
+
| ----------------- | ----------------------- | ------- | ------------------------------------------------------------------------------------------- |
|
|
994
|
+
| `HM3_ITEM_FIELDS` | `const HM3_ITEM_FIELDS` | — | reading every HM3 item type's shared-source → `hm3.system` field mapping, in emission order |
|
|
995
|
+
|
|
996
|
+
### `hm3.documentSubtypes`
|
|
997
|
+
|
|
998
|
+
HM3's note-type → document-subtype map. Unlike SoHL's near-identity map, HM3's differs substantially: four rows are one-to-many (authored via `hm3.type`, never inferred), one renames outright, and five type names are shared with SoHL but back a different data model — so each system resolves through its own map and its own registry rather than matching by name.
|
|
999
|
+
|
|
1000
|
+
| Export | Signature | Returns | Use it when |
|
|
1001
|
+
| ----------------------- | ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1002
|
+
| `HM3_TYPE_KEY` | `const HM3_TYPE_KEY` | — | reading the frontmatter key (`hm3.type`) inside the `hm3:` block that resolves a one-to-many content type to its HM3 document subtype |
|
|
1003
|
+
| `HM3_DOCUMENT_SUBTYPES` | `const HM3_DOCUMENT_SUBTYPES` | — | looking up which Foundry document and subtype a content type compiles into under HM3; a type absent from this map compiles into no HM3 document at all |
|
|
1004
|
+
|
|
1005
|
+
### `hm3.items`
|
|
1006
|
+
|
|
1007
|
+
HM3's Item pass — the parts of compiling a note into an HM3 Item that are facts about HM3: the `description` key rendered from a note's `{#appearance}` section (the one HM3 Item field with nowhere else to put it), and the `flags.hm3.templatePriority` flag HM3's data model has no field for. There is no HM3 equivalent of SoHL's `docHtml` — the prose still compiles into its JournalEntry, it just isn't addressed from the item.
|
|
1008
|
+
|
|
1009
|
+
| Export | Signature | Returns | Use it when |
|
|
1010
|
+
| ---------- | ---------------- | ------- | ------------------------------------------------------------------ |
|
|
1011
|
+
| `Hm3Items` | `class Hm3Items` | — | not called directly — imported and driven by `engine/generate.mjs` |
|
|
1012
|
+
|
|
1013
|
+
### `hm3.actors`
|
|
1014
|
+
|
|
1015
|
+
HM3's Actor pass — what an HM3 `character` or `creature` holds. A being note authors which HM3 subtype it becomes via `hm3.type` (never defaulted), and only four of the content format's `being` mapping-table rows have an HM3 destination (`portrait`, `species`, `gender`, `occupation`, `templatePriority`) plus the two anchored prose sections (`{#appearance}` → `description`, `{#dossier}` → `biography`); `gender` and `occupation` are written only on a `character`.
|
|
1016
|
+
|
|
1017
|
+
| Export | Signature | Returns | Use it when |
|
|
1018
|
+
| ----------- | ----------------- | ------- | ------------------------------------------------------------------ |
|
|
1019
|
+
| `Hm3Actors` | `class Hm3Actors` | — | not called directly — imported and driven by `engine/generate.mjs` |
|
|
1020
|
+
|
|
1021
|
+
### `hm3.templatePriority`
|
|
1022
|
+
|
|
1023
|
+
Where HM3 records the template priority — one statement (`data.templatePriority`), read through the shared field resolver by both of this system's passes so they cannot disagree about it, and written into `flags` because HM3's data model declares no field for it.
|
|
1024
|
+
|
|
1025
|
+
| Export | Signature | Returns | Use it when |
|
|
1026
|
+
| --------------- | -------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
1027
|
+
| `templateFlags` | `templateFlags(fm, block)` | `object` — the flags to emit | computing a document's `flags` object: whatever the note authors, plus HM3's template-priority flag when the note declares one |
|
|
1028
|
+
|
|
1029
|
+
### Flat exports (not under a namespace)
|
|
1030
|
+
|
|
1031
|
+
| Export | Signature | Returns | Use it when |
|
|
1032
|
+
| ---------------------- | ---------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
1033
|
+
| `HM3_DEFAULT_ITEM_ART` | `const HM3_DEFAULT_ITEM_ART` | — | reading the full map of default item artwork keyed by **note** type (not document subtype) for HM3 items |
|
|
1034
|
+
| `hm3DefaultItemArt` | `hm3DefaultItemArt(type)` | `string` — the default image path | getting the default art path for an HM3 item's note type, throwing when the type is unknown |
|
|
1035
|
+
|
|
1036
|
+
## `./content-config`
|
|
1037
|
+
|
|
1038
|
+
The per-repository configuration contract: what `package-build.config.yaml` (or an `.mjs` config calling `defineConfig` directly) must satisfy. `defineConfig` validates and normalizes the whole thing, returning a deeply frozen copy, and performs no I/O — `engine.packConfig` is what locates and reads a repository's file, deriving the three fields absent from authored YAML (`rootDir`, `stats.systemVersion`, the resolved `itemBuilders` table). See `docs/configuration.md` for the full key-by-key reference; this section covers the module's own exported shape.
|
|
1039
|
+
|
|
1040
|
+
```js
|
|
1041
|
+
import { defineConfig, PACKAGE_KINDS } from "@heroiclands/package-build/content-config";
|
|
1042
|
+
|
|
1043
|
+
const config = defineConfig({
|
|
1044
|
+
rootDir: import.meta.dirname,
|
|
1045
|
+
contentPackage: "example",
|
|
1046
|
+
foundryPackage: "example",
|
|
1047
|
+
packageKind: PACKAGE_KINDS[0], // "systems"
|
|
1048
|
+
stats: { lastModifiedBy: "examplebuilder00" },
|
|
1049
|
+
packs: [{ name: "items", type: "Item" }],
|
|
1050
|
+
compatibility: { minimum: "14.359" },
|
|
1051
|
+
});
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
| Export | Signature | Returns | Use it when |
|
|
1055
|
+
| -------------------------- | ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1056
|
+
| `defineConfig` | `defineConfig(config)` | `ContentBuildConfig` — the frozen, defaulted configuration | validating and normalizing a content configuration; every configuration reaches this function whether authored as YAML or as `.mjs` |
|
|
1057
|
+
| `PACKAGE_KINDS` | `const PACKAGE_KINDS` | — | reading the three kinds of Foundry package a content module can be built into — `systems`, `modules`, and `documentation`, the kind that compiles no Foundry document at all (also the Foundry install directory name for the first two) |
|
|
1058
|
+
| `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND` | — | naming the `packageKind` value that compiles no Foundry documents, spelled once so the validator, the CLI and the compile passes cannot disagree about what it means |
|
|
1059
|
+
| `compilesFoundryDocuments` | `compilesFoundryDocuments(config)` | `boolean` | checking whether a resolved configuration compiles Foundry documents at all — false only for a `documentation` package |
|
|
1060
|
+
| `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES` | — | reading the Foundry document types this toolchain is able to compile a compendium pack of |
|
|
1061
|
+
| `DEFAULT_PATHS` | `const DEFAULT_PATHS` | — | reading the conventional directory layout a build reads from and writes to, relative to `rootDir` |
|
|
1062
|
+
| `DEFAULT_ADDRESS_SCHEME` | `const DEFAULT_ADDRESS_SCHEME` | — | reading an unconfigured repository's address-scheme defaults (`prefix`, where the content tree mounts inside the package) |
|
|
1063
|
+
| `RETIRED_ADDRESS_KEYS` | `const RETIRED_ADDRESS_KEYS` | — | reading which address-scheme keys a configuration may no longer declare (e.g. `landing`) — declaring one is a refusal, not a silent no-op |
|
|
1064
|
+
| `SITE_MODES` | `const SITE_MODES` | — | reading the publishing modes `publish.site` may name, weakest first |
|
|
1065
|
+
| `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
|
|
1066
|
+
| `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether a resolved configuration publishes the pages its content tree compiles to — the one question the site build and the content index both need answered identically |
|
|
1067
|
+
|
|
1068
|
+
## `./config`
|
|
1069
|
+
|
|
1070
|
+
The per-repository **packaging** configuration — `packageBuild:` within the same `package-build.config.yaml` that `content-config` validates the rest of. Read through the content half's loader rather than a second file, so `packageKind` and `foundryPackage` are stated once. The two halves validate independently but share one document.
|
|
1071
|
+
|
|
1072
|
+
```js
|
|
1073
|
+
// Run from a consuming repository's root, where package-build.config.yaml lives.
|
|
1074
|
+
import { loadPackageBuildConfig } from "@heroiclands/package-build/config";
|
|
1075
|
+
|
|
1076
|
+
const config = loadPackageBuildConfig();
|
|
1077
|
+
console.log(config.stageDir);
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
| Export | Signature | Returns | Use it when |
|
|
1081
|
+
| --------------------------- | ----------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1082
|
+
| `DERIVED_MANIFEST_KEYS` | `const DERIVED_MANIFEST_KEYS` | — | reading which manifest keys a repository may not declare because the build derives them (declaring one is an error naming the key) |
|
|
1083
|
+
| `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)` | `Readonly<PackageBuildConfig>` | validating the `packageBuild:` section from an already-loaded shared configuration — the pure half, usable without touching disk |
|
|
1084
|
+
| `loadPackageBuildConfig` | `loadPackageBuildConfig()` | `Readonly<PackageBuildConfig>` | reading and validating the repository's resolved package-build configuration from disk, read fresh on each call rather than cached at import |
|
|
1085
|
+
|
|
1086
|
+
## `./prettier`
|
|
1087
|
+
|
|
1088
|
+
The shared Prettier configuration, published as a Prettier config module so an editor's format-on-save agrees with `content-build format`, which already applies these rules without a consumer declaring anything.
|
|
1089
|
+
|
|
1090
|
+
```js
|
|
1091
|
+
// prettier.config.mjs
|
|
1092
|
+
export { default } from "@heroiclands/package-build/prettier";
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
| Export | Signature | Returns | Use it when |
|
|
1096
|
+
| --------- | -------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
1097
|
+
| `default` | `export default PRETTIER_CONFIG` | The shared Prettier configuration object | re-exporting it as a consumer's own `prettier.config.mjs`, so an editor and the lint chain agree |
|
|
1098
|
+
|
|
1099
|
+
## `./markdownlint`
|
|
1100
|
+
|
|
1101
|
+
The shared markdownlint rules, as a markdownlint-cli2 options module, applied without a consumer declaring anything by `content-build markdown`. Exists for an editor's markdownlint extension, and for a consumer that wants to extend rather than replace the set.
|
|
1102
|
+
|
|
1103
|
+
```js
|
|
1104
|
+
// .markdownlint-cli2.mjs
|
|
1105
|
+
import shared from "@heroiclands/package-build/markdownlint";
|
|
1106
|
+
export default { ...shared, config: { ...shared.config, MD013: true } };
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
| Export | Signature | Returns | Use it when |
|
|
1110
|
+
| --------- | ------------------------------------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1111
|
+
| `default` | `export default { config, globs, ignores, gitignore: true }` | The shared markdownlint-cli2 options object | spreading it into a consumer's own options module to extend rather than replace the shared rules; `config` is frozen, so it must be spread rather than mutated |
|
|
1112
|
+
|
|
1113
|
+
## `./bundle`
|
|
1114
|
+
|
|
1115
|
+
The code bundle, and the one way a manifest can disagree with it. Which manifest key an entry is declared under decides how the browser parses the file — an ES module, where every top-level declaration is scoped to the module, or a classic script, where those same declarations become global lexical bindings that can collide with a non-configurable `window` property and throw a parse-time `SyntaxError`. The check compares what the manifest declares against what the bundle actually is, rather than checking the manifest key in isolation. Every rule here is a pure function over source text; reading the built files is the caller's job.
|
|
1116
|
+
|
|
1117
|
+
```js
|
|
1118
|
+
import { globalDeclarations } from "@heroiclands/package-build/bundle";
|
|
1119
|
+
|
|
1120
|
+
console.log(globalDeclarations("const chrome = 1;").map((d) => d.name));
|
|
1121
|
+
// -> ["chrome"]
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
| Export | Signature | Returns | Use it when |
|
|
1125
|
+
| -------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
1126
|
+
| `declaredGlobals` | `declaredGlobals(node)` | `string[]` — declared identifier names, empty when the statement declares none | inspecting one top-level `Program.body` entry for what it would declare in global scope |
|
|
1127
|
+
| `entryDeclaration` | `entryDeclaration(manifest, entry)` | `"esmodules"\|"scripts"\|"both"\|"neither"` | finding out how a parsed manifest declares a given entry file |
|
|
1128
|
+
| `globalDeclarations` | `globalDeclarations(source)` | `Array<{name: string, line: number, kind: string}>` | listing every top-level declaration a bundle's source text would create under a classic-script parse |
|
|
1129
|
+
| `checkBundleLoading` | `checkBundleLoading({ manifest, source, entry, manifestName = "the manifest" })` | `{findings: Array<{line?: number, severity: "error", message: string}>, declaredAs: "esmodules"\|"scripts"\|"both"\|"neither"}` | verifying a manifest and its bundle agree — the findings are empty when they do |
|
|
1130
|
+
|
|
1131
|
+
## `./container`
|
|
1132
|
+
|
|
1133
|
+
Running a built package inside a Foundry VTT container. `package-build deploy <stage>` installs a staged package into `FOUNDRYVTT_<STAGE>_DATA`; this module bind-mounts that same directory at `/data` in the community `felddy/foundryvtt` image and serves it, so running Foundry against what was just deployed is the next step from one variable. Licensing, provisioning and the Foundry build itself are left to the image and to environment passthrough — every `FOUNDRY_*` and `CONTAINER_*` variable is passed through — rather than being reimplemented here. The resolution rules (`resolve*`) are pure functions over data; the functions that talk to `docker` or the filesystem are named for it.
|
|
1134
|
+
|
|
1135
|
+
```js
|
|
1136
|
+
import { containerName, dataEnvVar } from "@heroiclands/package-build/container";
|
|
1137
|
+
|
|
1138
|
+
console.log(containerName("sohl", "dev"));
|
|
1139
|
+
// -> "sohl-foundry-dev"
|
|
1140
|
+
console.log(dataEnvVar("dev"));
|
|
1141
|
+
// -> "FOUNDRYVTT_DEV_DATA"
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
| Export | Signature | Returns | Use it when |
|
|
1145
|
+
| -------------------------- | --------------------------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1146
|
+
| `DEFAULT_STAGE_PORTS` | `const DEFAULT_STAGE_PORTS` | — | reading the host port each conventional stage (`dev`/`qa`/`prod`/`test`) publishes by default, chosen so all four can run at once |
|
|
1147
|
+
| `CONTAINER_PORT` | `const CONTAINER_PORT` | — | the fixed port Foundry listens on inside the container |
|
|
1148
|
+
| `CACHE_MOUNT` | `const CACHE_MOUNT` | — | the path a host-provided download cache is mounted at inside the container |
|
|
1149
|
+
| `CONTAINER_ACTIONS` | `const CONTAINER_ACTIONS` | — | the closed set of actions `package-build container` accepts |
|
|
1150
|
+
| `dataEnvVar` | `dataEnvVar(stage)` | `string` | deriving the environment variable that names a stage's Foundry data root, without hand-tabulating one per stage |
|
|
1151
|
+
| `containerName` | `containerName(packageId, stage, base = null)` | `string` | computing the stable container name a stage runs under, so a signed Foundry licence (bound to the hostname) survives a recreate; pass `base` to share one container/licence across packages |
|
|
1152
|
+
| `resolveStagePort` | `resolveStagePort(stage, { env, stages } = {})` | `number` | resolving the host port for a stage, honoring `FOUNDRYVTT_<STAGE>_PORT` over a declared or conventional default |
|
|
1153
|
+
| `resolveFoundryVersion` | `resolveFoundryVersion(stage, { env, stages, compatibilityMinimum, e2eStage = "test" } = {})` | `string\|null` | pinning the exact Foundry build a stage runs, deriving the end-to-end stage's pin from the package's own `compatibility.minimum` so the floor is actually exercised |
|
|
1154
|
+
| `resolveImage` | `resolveImage({ env, image, version, compatibilityMinimum } = {})` | `string` | resolving the `felddy/foundryvtt` image reference a run uses |
|
|
1155
|
+
| `resolveWorld` | `resolveWorld(stage, { env, stages } = {})` | `string\|null` | resolving which world a stage auto-launches — `""` declares "never auto-launch", distinct from `null` (leave `FOUNDRY_WORLD` alone) |
|
|
1156
|
+
| `resolveLicenseKey` | `resolveLicenseKey(stage, env = process.env)` | `string\|null` | finding a stage's dedicated Foundry licence key, since Foundry is single-seat and two stages running at once need two keys |
|
|
1157
|
+
| `resolveDataRoot` | `resolveDataRoot(stage, { env } = {})` | `string` | resolving the local Foundry data root path for a stage, checked for what a bind mount needs |
|
|
1158
|
+
| `passthroughEnv` | `passthroughEnv(env = process.env)` | `[string, string][]` | collecting the image's own `FOUNDRY_*`/`CONTAINER_*` environment pairs to pass through (deliberately excludes `CONTAINER_CACHE`, which `dockerRunArgs` sets itself) |
|
|
1159
|
+
| `dockerRunArgs` | `dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, version, world, licenseKey })` | `string[]` | building the full `docker run` argument vector for a stage's container |
|
|
1160
|
+
| `runDocker` | `runDocker(args)` | `number` | running the `docker` CLI with inherited stdio and getting back its exit status |
|
|
1161
|
+
| `captureDocker` | `captureDocker(args)` | `string` | running `docker` and capturing trimmed stdout, tolerating failure (returns `""`) |
|
|
1162
|
+
| `containerExists` | `containerExists(name, runningOnly = false)` | `boolean` | checking whether a container with exactly this name exists (optionally, only counting a running one) |
|
|
1163
|
+
| `runningFoundryContainers` | `runningFoundryContainers(except = "")` | `string[]` | listing HeroicLands-convention Foundry containers currently running, to warn about a licence clash before a run starts |
|
|
1164
|
+
| `clearStaleLock` | `clearStaleLock(dataRoot, log = () => {})` | — | removing a data-root lock left behind by a container that did not shut down cleanly; only safe to call while the container is stopped |
|
|
1165
|
+
| `resolveContainer` | `resolveContainer({ stage, config, env = process.env })` | `ResolvedContainer` | resolving all of a stage's container settings in one call, from the package-build configuration |
|
|
1166
|
+
| `startContainer` | `startContainer(container, { dataRoot, env, log })` | `number` | starting a stage's container, creating it first if it does not exist |
|
|
1167
|
+
| `removeContainer` | `removeContainer(name, log = () => {})` | — | stopping and removing a container, tolerating "not running" and "no such container" |
|
|
1168
|
+
| `containerAction` | `containerAction({ action, stage, config, env, log })` | `number` | performing one `CONTAINER_ACTIONS` action for a stage end to end |
|
|
1169
|
+
|
|
1170
|
+
## `./coverage`
|
|
1171
|
+
|
|
1172
|
+
Whether the localization keys a package **references** and the keys it **declares** are the same set. A referenced-but-undeclared key is an error — it renders as its raw key string to a player; a declared-but-unreferenced key is advisory only, since no scan can see every way a key might be reached. Generic Foundry reference shapes (`{{localize}}`, `game.i18n.localize`, `LOCALIZATION_PREFIXES`, template literals) are read here directly; a repository that mints keys by its own convention contributes a named module through configuration. Everything exported is pure — source text in, references or findings out.
|
|
1173
|
+
|
|
1174
|
+
```js
|
|
1175
|
+
import { keyRootsOf } from "@heroiclands/package-build/coverage";
|
|
1176
|
+
|
|
1177
|
+
console.log(keyRootsOf(["SOHL.Item.Name", "SOHL.Actor.Name"]));
|
|
1178
|
+
// -> ["SOHL"]
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
| Export | Signature | Returns | Use it when |
|
|
1182
|
+
| --------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
1183
|
+
| `keyRootsOf` | `keyRootsOf(keys)` | `string[]` | deriving the distinct first segments a set of declared localization keys uses, instead of configuring roots by hand |
|
|
1184
|
+
| `collectScriptReferences` | `collectScriptReferences(source, { file, roots })` | `ReferenceSet` | reading every localization reference out of a script's AST (so a key named only in a JSDoc `@example` is not counted) |
|
|
1185
|
+
| `collectTemplateReferences` | `collectTemplateReferences(source, { file, roots })` | `ReferenceSet` | reading every localization reference out of a Handlebars template by text scan |
|
|
1186
|
+
| `mergeReferences` | `mergeReferences(sets)` | `ReferenceSet` | combining multiple files' reference sets into one before comparing against declared keys |
|
|
1187
|
+
| `analyzeCoverage` | `analyzeCoverage({ langSource, langFile, references, retained = [], roots })` | `{findings: CoverageFinding[], unreferenced: CoverageFinding[], stats: object}` | comparing what a package declares against what it references, separating must-fix findings from merely-advisory unreferenced keys |
|
|
1188
|
+
|
|
1189
|
+
## `./deploy`
|
|
1190
|
+
|
|
1191
|
+
Deploying a staged package into a Foundry data directory, always via a staged, atomic swap rather than a write in place — because a running Foundry holds its LevelDB compendium packs open, and replacing pack files underneath a live server corrupts them. The build lands in a sibling `…-staging-<pid>` directory and is renamed into place. Two transports are chosen from the destination string itself: a local path is copied, and a `[user@]host:/path` target is uploaded over SFTP, authenticating through the running SSH agent by default with no secret read from disk. The resolution rules are pure functions over data; the functions that touch a filesystem or network are named for it.
|
|
1192
|
+
|
|
1193
|
+
```js
|
|
1194
|
+
import { isRemoteTarget, parseRemote } from "@heroiclands/package-build/deploy";
|
|
1195
|
+
|
|
1196
|
+
console.log(isRemoteTarget("user@example.org:/srv/foundry"));
|
|
1197
|
+
// -> true
|
|
1198
|
+
console.log(parseRemote("user@example.org:/srv/foundry"));
|
|
1199
|
+
// -> { username: "user", host: "example.org", remotePath: "/srv/foundry" }
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
| Export | Signature | Returns | Use it when |
|
|
1203
|
+
| ----------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1204
|
+
| `STAGE_ENV_MAP` | `const STAGE_ENV_MAP` | — | reading the environment variable that names each stage's Foundry data root |
|
|
1205
|
+
| `resolveStage` | `resolveStage(stageArg)` | `string` | normalising a raw stage argument to its trimmed, lowercased form (`""` when absent) |
|
|
1206
|
+
| `packageSubpath` | `packageSubpath(packageKind, packageId)` | `string[]` | deriving where a package installs beneath a Foundry data root, from its kind and id |
|
|
1207
|
+
| `isRemoteTarget` | `isRemoteTarget(target)` | `boolean` | deciding whether a configured destination is a remote SFTP target or a local directory (correctly excluding a Windows drive letter like `C:\Foundry\Data`) |
|
|
1208
|
+
| `parseRemote` | `parseRemote(target)` | `{username: string\|undefined, host: string, remotePath: string}` | splitting a `[user@]host:/path` remote target into its parts |
|
|
1209
|
+
| `resolveAgent` | `resolveAgent(env, stageUpper, prefix = "SOHL")` | `string\|undefined` | locating the SSH agent endpoint cross-platform, falling back to a key file when `undefined` |
|
|
1210
|
+
| `buildConnection` | `async buildConnection(stageUpper, remote, { env, prefix = "SOHL" } = {})` | `Promise<object>` | assembling an `ssh2-sftp-client` connection config for a stage, defaulting to agent auth so no secret is read from disk |
|
|
1211
|
+
| `deployLocal` | `async deployLocal(srcAbs, destDir)` | `Promise<void>` | mirroring a staged build into a local directory via the staged, atomic swap |
|
|
1212
|
+
| `deployRemote` | `async deployRemote(conn, srcAbs, remoteDir, { onUpload } = {})` | `Promise<void>` | mirroring a staged build into a remote directory over SFTP, with the same staged swap `deployLocal` performs |
|
|
1213
|
+
| `deployStage` | `async deployStage({ stage, source, packageKind, packageId, env, prefix = "SOHL", log })` | `Promise<{stage: string, destination: string, remote: boolean}>` | deploying a staged package to one named stage, choosing the transport from the configured destination automatically |
|
|
1214
|
+
|
|
1215
|
+
## `./e2e`
|
|
1216
|
+
|
|
1217
|
+
The end-to-end harness: a disposable Foundry world, served from a container, with a browser suite driven against it. The harness does not know what the suite is — what runs against the served world is named in `packageBuild.e2e.suite` — it only stands the licensed Foundry up, seeds a world with a known Gamemaster password, waits for that world to be _active_, and tears it down again. Three shapes of run answer different questions: `run` (from scratch, the only path that may change Foundry build), `fast` (the iteration loop — rebuild, redeploy, cycle, re-run), and `sweep` (the same full run against a build the repository does not pin, so `compatibility.verified` can be evidence rather than hope).
|
|
1218
|
+
|
|
1219
|
+
```js
|
|
1220
|
+
import { hashPassword, E2E_MODES } from "@heroiclands/package-build/e2e";
|
|
1221
|
+
|
|
1222
|
+
console.log(hashPassword("pw", "salt").slice(0, 8));
|
|
1223
|
+
// -> "dba2956d"
|
|
1224
|
+
console.log(E2E_MODES);
|
|
1225
|
+
// -> ["run", "open"]
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
| Export | Signature | Returns | Use it when |
|
|
1229
|
+
| ----------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1230
|
+
| `E2E_GM_ID` | `const E2E_GM_ID` | — | reading the fixed Gamemaster document id every seeded world carries |
|
|
1231
|
+
| `E2E_SCENE_ID` | `const E2E_SCENE_ID` | — | reading the seeded, pre-activated default scene's id |
|
|
1232
|
+
| `E2E_MODES` | `const E2E_MODES` | — | the closed set of modes the suite may run under (`"run"` / `"open"`) |
|
|
1233
|
+
| `hashPassword` | `hashPassword(password, salt)` | `string` — the hex hash | hashing a password the way Foundry's own `core/auth.mjs` does, to seed a login that actually works |
|
|
1234
|
+
| `resolveE2EWorld` | `resolveE2EWorld(config, env = process.env)` | `E2EWorld` | resolving the seeded world's identity from configuration, with an environment override under the repository's own variable prefix |
|
|
1235
|
+
| `worldManifest` | `worldManifest({ worldId, worldTitle, worldDescription, systemId, systemVersion, coreVersion })` | `object` — the world manifest | building the `world.json` a seeded world carries |
|
|
1236
|
+
| `gmDocument` | `gmDocument({ id, name, password, salt })` | `object` — the user document | building the seeded world's single Gamemaster user document |
|
|
1237
|
+
| `defaultSceneDocument` | `defaultSceneDocument()` | `object` — the scene document | building the one pre-activated scene every seeded world carries |
|
|
1238
|
+
| `moduleConfigurationDocument` | `moduleConfigurationDocument(packageId)` | `object` — the setting document | building the world setting that switches a module package on (a module must be activated; a system does not) |
|
|
1239
|
+
| `isWorldActive` | `isWorldActive(body)` | `boolean` | checking whether a `/join` response body shows a world that is actually active, not merely reachable |
|
|
1240
|
+
| `resolveSweepVersion` | `resolveSweepVersion(argv)` | `string` — the exact build, trimmed | reading the exact Foundry build a sweep was asked to run against; there is deliberately no default |
|
|
1241
|
+
| `parseFastArgs` | `parseFastArgs(argv, build)` | `FastArgs` | parsing the fast loop's CLI arguments against the repository's declared build table, in declaration order |
|
|
1242
|
+
| `seedTestWorld` | `async seedTestWorld({ config, packageJson, env = process.env, log = () => {} })` | `Promise<{worldDir: string, world: E2EWorld}>` | seeding the disposable world into the end-to-end stage's data root, wiping and rewriting the world directory each time |
|
|
1243
|
+
| `waitForWorld` | `async waitForWorld({ url, container, stage, timeoutMs = 180_000, log = () => {} })` | `Promise<void>` | polling until a seeded world is active, or failing with a diagnosis (a licence failure is detected from the container log, not a timeout) |
|
|
1244
|
+
| `suiteExecutables` | `suiteExecutables(command)` | `string[]` | reading every executable a suite command needs to exist before it can run at all |
|
|
1245
|
+
| `findExecutable` | `findExecutable(name, { cwd, env = process.env } = {})` | `string\|null` | finding an executable the way the child process would (`node_modules/.bin` first, then `PATH`) |
|
|
1246
|
+
| `missingExecutables` | `missingExecutables({ command, cwd, env = process.env })` | `string[]` | checking which of a suite command's executables are not there — asked again mid-run, since an install can pull the runner out from under a run in progress |
|
|
1247
|
+
| `freshResults` | `freshResults({ paths, since, cwd })` | `string[]` | finding which declared result paths a suite run actually wrote to since it was spawned (existence alone is not evidence) |
|
|
1248
|
+
| `suiteVerdict` | `suiteVerdict({ status, vanished = [], declared = [], fresh = [] })` | `SuiteVerdict` | deciding what a finished suite run is worth given its exit status and what it left behind; can only ever downgrade a verdict, never upgrade one |
|
|
1249
|
+
| `runSuite` | `runSuite({ command, args = [], cwd, results = [], env = process.env, log = () => {} })` | `number` — the suite's exit status | running the repository's suite, bracketed by before/after executable checks and a fresh-results check rather than trusting the exit status alone |
|
|
1250
|
+
| `suiteCommand` | `suiteCommand(config, mode)` | `string[]` | reading the declared suite command for a mode (`"run"` or `"open"`), or failing clearly when none is declared |
|
|
1251
|
+
| `e2eRun` | `async e2eRun({ config, packageJson, mode = "run", suiteArgs = [], env = process.env, log = () => {} })` | `Promise<number>` | running a full, from-scratch end-to-end pass: deploy, reseed, recreate the container, wait, run the suite, tear down (or leave it serving in `"open"` mode) |
|
|
1252
|
+
| `e2eFast` | `async e2eFast({ config, argv = [], env = process.env, log = () => {} })` | `Promise<number>` | iterating quickly during development: rebuild what changed, redeploy, cycle the world, wait, re-run |
|
|
1253
|
+
| `e2eSweep` | `async e2eSweep({ config, packageJson, argv = [], env = process.env, log = () => {} })` | `Promise<number>` | running the full suite against a build the repository does not pin, to license moving `compatibility.verified` forward |
|
|
1254
|
+
|
|
1255
|
+
## `./lang`
|
|
1256
|
+
|
|
1257
|
+
What a shippable Foundry localization file must satisfy. Every HeroicLands package ships `lang/*.json`, and each way it can be malformed fails silently at runtime rather than at build time: a file that is not a plain object, a dotted-prefix key collision that makes `foundry.utils.expandObject` throw and Foundry discard the entire file, a Handlebars placeholder that renders literally, or non-identifier data baked into a key segment. Both functions here are pure — source text in, findings out — leaving discovery, I/O and reporting to the caller.
|
|
1258
|
+
|
|
1259
|
+
```js
|
|
1260
|
+
import { validateLangSource } from "@heroiclands/package-build/lang";
|
|
1261
|
+
|
|
1262
|
+
console.log(validateLangSource('{"SOHL": {"Name": "Test"}}'));
|
|
1263
|
+
// -> [] (no findings: the file is valid)
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
| Export | Signature | Returns | Use it when |
|
|
1267
|
+
| ---------------------- | ---------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1268
|
+
| `findPrefixCollisions` | `findPrefixCollisions(json)` | `[string, string][]` | finding every `[prefixKey, leafKey]` pair where one key is a strict dotted prefix of another — the exact shape that makes `foundry.utils.expandObject` throw |
|
|
1269
|
+
| `validateLangSource` | `validateLangSource(raw)` | `LangFinding[]` — empty when the file is shippable | validating one localization file's source text against every silent-failure shape at once |
|
|
1270
|
+
|
|
1271
|
+
## `./manifest`
|
|
1272
|
+
|
|
1273
|
+
Building the Foundry package manifest — `system.json` or `module.json` — generated rather than hand-stamped from a template, so the pack list, the identity and the compatibility ranges are stated once, in the configuration, and never restated in a second format that can silently disagree. Three kinds of key end up in the result: declared (`packageBuild.manifest`, emitted unchanged), derived (identity, release addresses, version, compatibility, pack list — an authored copy of these is refused as an error), and computed (namespaced `flags`). The rules are pure functions over data; I/O is confined to `writeManifest`.
|
|
1274
|
+
|
|
1275
|
+
```js
|
|
1276
|
+
import { normalizeRepoUrl } from "@heroiclands/package-build/manifest";
|
|
1277
|
+
|
|
1278
|
+
console.log(normalizeRepoUrl("git@github.com:HeroicLands/sohl.git"));
|
|
1279
|
+
// -> "git@github.com:HeroicLands/sohl"
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
| Export | Signature | Returns | Use it when |
|
|
1283
|
+
| ------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1284
|
+
| `ARTIFACTS` | `const ARTIFACTS` | — | reading the two Foundry package kinds (`"system"`, `"module"`) as the artifact name their manifest and release archive are called |
|
|
1285
|
+
| `normalizeRepoUrl` | `normalizeRepoUrl(repository)` | `string` — normalised `https://` URL, no trailing slash | normalising `package.json`'s `repository` field (object or shorthand string, `git+…git` or plain) to the exact URL Foundry fetches release assets from |
|
|
1286
|
+
| `releaseUrls` | `releaseUrls({ repoUrl, version, artifact })` | `{url: string, bugs: string, manifest: string, download: string}` | building the manifest's release addresses — `manifest` points at `releases/latest` so an installed package can discover updates, `download` is pinned to this version |
|
|
1287
|
+
| `metadataUrl` | `metadataUrl({ repoUrl, version, contentPackage })` | `string` — the version-pinned asset URL | computing where this release publishes its content index, pinned to this version like `download` so a dependency's manifest and its fetched index always describe the same release |
|
|
1288
|
+
| `manifestPacks` | `manifestPacks(config)` | `object[]` | deriving the manifest's `packs` array from the one pack list the build already has, in build order, companions flattened in |
|
|
1289
|
+
| `packFolderFindings` | `packFolderFindings({ packFolders, packs = [] })` | `Array<{severity: "error"\|"warning", message: string, pack: string, folder?: string, keyPath: Array<string\|number>}>` | checking a declared `packFolders` against the derived pack list — a folder naming a pack that does not exist is an error, a pack no folder names is a warning |
|
|
1290
|
+
| `BUILD_ONLY_RELATIONSHIP_KEYS` | `const BUILD_ONLY_RELATIONSHIP_KEYS` | — | reading which `relationships` keys direct the build (e.g. `itemCatalog`) rather than describe the published package, so they can be filtered out of what ships |
|
|
1291
|
+
| `publishedRelationships` | `publishedRelationships(relationships)` | `Record<string, unknown>` | producing the `relationships` block as it should be published, with `BUILD_ONLY_RELATIONSHIP_KEYS` dropped and everything else preserved in order |
|
|
1292
|
+
| `buildManifest` | `buildManifest({ config, packageJson, artifact, flags })` | `object` — the manifest, ready to serialise | assembling the full Foundry package manifest object from the resolved configuration, without writing it anywhere |
|
|
1293
|
+
| `writeManifest` | `async writeManifest({ config, packageJson, artifact, outDir, flags, configFile })` | `Promise<{path: string, manifest: object}>` | building the manifest and writing it into the staged package; refuses to write when `packFolders` names a pack the build does not produce |
|
|
1294
|
+
|
|
1295
|
+
## `./release`
|
|
1296
|
+
|
|
1297
|
+
The release archive — the two (or three) files a Foundry package's GitHub Release carries: `<artifact>.zip` (the staged tree), `<artifact>.json` (the manifest, re-fetched by an already-installed package to notice updates), and, when the package ships content, a published content index other packages resolve addresses through. Kept apart from `stage.mjs` because this is the only part of assembly that needs a dependency (the zip archiver); a repository that never cuts a release from a local build pays nothing for it.
|
|
1298
|
+
|
|
1299
|
+
```js
|
|
1300
|
+
import { packRelease } from "@heroiclands/package-build/release";
|
|
1301
|
+
|
|
1302
|
+
try {
|
|
1303
|
+
await packRelease({ stageDir: "build/stage", outDir: "build/dist" });
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
// Thrown when the stage has no manifest — nothing has been built yet.
|
|
1306
|
+
console.log(err.message);
|
|
1307
|
+
}
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
| Export | Signature | Returns | Use it when |
|
|
1311
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1312
|
+
| `packRelease` | `async packRelease({ stageDir = "build/stage", outDir = "build/dist", artifact = "system", metadataDir = "build/content-index", pdf = true } = {})` | `Promise<{zip: string, manifest: string, metadata?: string, pdf?: string, pdfFindings: array, pdfSkipped: string\|null, bytes: number, version: string}>` | zipping the staged tree and placing the manifest (and, unless `pdf: false`, the book) beside the archive; throws when the stage has no manifest to release |
|
|
1313
|
+
|
|
1314
|
+
## `./stage`
|
|
1315
|
+
|
|
1316
|
+
The build stage — assembling the tree that becomes a Foundry package, and clearing it away again. A Foundry package is a manifest, some assets, compiled packs, and (if it ships code) a bundle; every HeroicLands repository assembles that directory the same way, so the copying, the cleaning and the archiving are one implementation driven by a per-repository _list_ rather than per-repository code. The missing-source guard fails loudly on a listed path that does not exist, rather than silently shipping a package with a missing `lang/` or `templates/`. The rules are pure functions over data; the functions that touch disk are named for the effect they have.
|
|
1317
|
+
|
|
1318
|
+
```js
|
|
1319
|
+
import { missingSources } from "@heroiclands/package-build/stage";
|
|
1320
|
+
|
|
1321
|
+
console.log(missingSources([["does/not/exist", "x"]]));
|
|
1322
|
+
// -> ["does/not/exist"]
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
| Export | Signature | Returns | Use it when |
|
|
1326
|
+
| --------------------- | ---------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1327
|
+
| `BUILD_ARTIFACT_DIRS` | `const BUILD_ARTIFACT_DIRS` | — | reading the directories every HeroicLands repository regenerates and none commits (`build`, `.vite`, `.vitepress`, `.rollup.cache`) |
|
|
1328
|
+
| `missingSources` | `missingSources(entries, cwd = process.cwd())` | `string[]` | checking a whole `[source, dest]` list for absent sources up front, so every problem is reported at once rather than one rebuild at a time |
|
|
1329
|
+
| `copyTree` | `copyTree(src, dest, { transform } = {})` | `number` — files written | recursively copying a file or directory, optionally rewriting each file's content as it's staged instead of copying bytes verbatim |
|
|
1330
|
+
| `stageAssets` | `stageAssets(entries, { cwd = process.cwd(), transform } = {})` | `{entries: number, files: number}` | copying every listed `[source, dest]` pair into the stage, refusing to start at all if any source is absent |
|
|
1331
|
+
| `cleanBuildArtifacts` | `cleanBuildArtifacts(root, { extra = [], includeNodeModules = false } = {})` | `string[]` — directories removed | removing the build artefacts a repository regenerates, safely repeatable since an already-clean directory is not an error |
|
|
1332
|
+
|
|
1333
|
+
## `./templates`
|
|
1334
|
+
|
|
1335
|
+
Whether a template's user-visible text goes through localization at all — the reverse of `./coverage`, which walks key → file and cannot see a template that names no key whatsoever; this walks text → key, checking that every user-visible literal in the markup is a `{{localize}}` call rather than English sitting in the file. It also compiles every template, because the common way to break one while localizing it — nesting `{{localize …}}` inside another mustache — is a parse error only inside a helper's hash, so the mistake ships from a template that looks exactly like its working neighbour. Both functions are pure: source text in, findings out.
|
|
1336
|
+
|
|
1337
|
+
```js
|
|
1338
|
+
import { VISIBLE_ATTRIBUTES } from "@heroiclands/package-build/templates";
|
|
1339
|
+
|
|
1340
|
+
console.log(VISIBLE_ATTRIBUTES);
|
|
1341
|
+
// -> ["title", "placeholder", "aria-label", "alt", "data-tooltip", "data-title"]
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
| Export | Signature | Returns | Use it when |
|
|
1345
|
+
| -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1346
|
+
| `VISIBLE_ATTRIBUTES` | `const VISIBLE_ATTRIBUTES` | — | reading the template attributes whose value a user actually reads (tooltip, placeholder, screen-reader text), so their English is checked as strictly as heading text |
|
|
1347
|
+
| `findHardcodedText` | `findHardcodedText(source, { allow = [] } = {})` | `TemplateFinding[]` | finding every user-visible literal a template leaves untranslated; `allow` is the explicit, reasoned escape hatch for a literal that is deliberately not a key |
|
|
1348
|
+
| `findTemplateSyntaxErrors` | `findTemplateSyntaxErrors(source)` | `TemplateFinding[]` — one finding when it does not parse, else none | checking whether Handlebars can parse a template at all, without needing any helper it calls to exist |
|
|
1349
|
+
|
|
1350
|
+
## `./package.json`
|
|
1351
|
+
|
|
1352
|
+
Not a JavaScript module — this subpath entry exists so tooling (bundlers, `import.meta.resolve`, a script reading the installed version) can resolve the package's own `package.json` through Node's package-exports resolution instead of reaching outside the declared export map.
|