@heroiclands/package-build 22.15.1 → 22.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +303 -42
  5. package/bin/report.mjs +1 -1
  6. package/ci/ci-docker.mjs +6 -3
  7. package/ci/ci-steps.mjs +6 -4
  8. package/ci/content-format-reference.mjs +106 -0
  9. package/config.mjs +22 -2
  10. package/content-config.mjs +74 -134
  11. package/docs/api.md +294 -234
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +33 -0
  14. package/docs/authoring/first-note.md +48 -0
  15. package/docs/authoring/frontmatter.md +40 -0
  16. package/docs/authoring/links-and-markup.md +137 -0
  17. package/docs/commands.md +631 -162
  18. package/docs/configuration.md +176 -140
  19. package/docs/content-format.md +50 -4229
  20. package/docs/design.md +32 -0
  21. package/docs/diagnostics.md +29 -22
  22. package/docs/getting-started.md +21 -877
  23. package/docs/guides/book.md +153 -0
  24. package/docs/guides/e2e.md +143 -0
  25. package/docs/guides/icons.md +126 -0
  26. package/docs/guides/localization.md +120 -0
  27. package/docs/guides/module-project.md +915 -0
  28. package/docs/guides/packs.md +167 -0
  29. package/docs/guides/reference-pages.md +148 -0
  30. package/docs/guides/release-and-deploy.md +114 -0
  31. package/docs/guides/site.md +152 -0
  32. package/docs/guides/system-project.md +45 -0
  33. package/docs/index.md +16 -0
  34. package/docs/project-setup.md +54 -35
  35. package/docs/reference/format-details.md +4420 -0
  36. package/docs/reference/note-types.md +531 -0
  37. package/engine/address-render.mjs +26 -0
  38. package/engine/address.mjs +3 -20
  39. package/engine/anchors.mjs +15 -6
  40. package/engine/asset-bindings.mjs +6 -2
  41. package/engine/base-compiler.mjs +47 -11
  42. package/engine/being-measurements.mjs +70 -0
  43. package/engine/calendar-artifacts.mjs +1 -1
  44. package/engine/calendar-choice.mjs +32 -0
  45. package/engine/calendar-human.mjs +71 -0
  46. package/engine/calendar-notes.mjs +41 -3
  47. package/engine/calendars.mjs +8 -1
  48. package/engine/changelog-lint.mjs +2 -2
  49. package/engine/code-fences.mjs +23 -97
  50. package/engine/compile-corpus.mjs +6 -7
  51. package/engine/content-admonitions.mjs +114 -0
  52. package/engine/content-captions.mjs +181 -0
  53. package/engine/content-embeds.mjs +2 -2
  54. package/engine/content-footnotes.mjs +34 -0
  55. package/engine/content-format-check.mjs +2 -2
  56. package/engine/content-format.mjs +127 -73
  57. package/engine/content-format.yaml +745 -0
  58. package/engine/content-html.mjs +1 -1
  59. package/engine/content-icons.mjs +45 -92
  60. package/engine/content-images.mjs +22 -74
  61. package/engine/content-index.mjs +12 -3
  62. package/engine/content-links.mjs +21 -26
  63. package/engine/content-lint.mjs +1 -5
  64. package/engine/content-secrets.mjs +3 -1
  65. package/engine/content-tables.mjs +47 -1257
  66. package/engine/culture-choice.mjs +32 -0
  67. package/engine/date-conversion.mjs +47 -0
  68. package/engine/declaration-check.mjs +87 -0
  69. package/engine/docs-checks.mjs +140 -0
  70. package/engine/documentation-links.mjs +31 -0
  71. package/engine/exported-scene.mjs +1 -1
  72. package/engine/extension-attributes.mjs +74 -0
  73. package/engine/field-reference.mjs +1 -1
  74. package/engine/field-spec.mjs +4 -0
  75. package/engine/foreign-catalog.mjs +5 -5
  76. package/engine/format-generated.mjs +15 -0
  77. package/engine/foundry-entries.mjs +3 -1
  78. package/engine/frontmatter-lint.mjs +5 -1
  79. package/engine/frontmatter.mjs +10 -0
  80. package/engine/generate.mjs +2 -2
  81. package/engine/helpers.mjs +71 -117
  82. package/engine/homepage.mjs +2 -3
  83. package/engine/index-records.mjs +1 -0
  84. package/engine/index.mjs +18 -3
  85. package/engine/infobox-registry.mjs +3 -1
  86. package/engine/infobox.mjs +38 -20
  87. package/engine/item-compiler.mjs +10 -8
  88. package/engine/item-docs.mjs +7 -6
  89. package/engine/journals.mjs +51 -10
  90. package/engine/macros.mjs +1 -1
  91. package/engine/map-build.mjs +1 -1
  92. package/engine/map-dot.mjs +1 -1
  93. package/engine/markdown-expressions.mjs +117 -0
  94. package/engine/metadata-index.mjs +7 -3
  95. package/engine/note-addresses.mjs +1 -0
  96. package/engine/note-dates.mjs +283 -61
  97. package/engine/note-package.mjs +5 -25
  98. package/engine/note-state.mjs +6 -2
  99. package/engine/note-vocabulary.mjs +74 -19
  100. package/engine/number-words.mjs +75 -0
  101. package/engine/office-holders.mjs +188 -0
  102. package/engine/pack-config.mjs +2 -15
  103. package/engine/pack-router.mjs +24 -5
  104. package/engine/packages.mjs +3 -2
  105. package/engine/pathnames.mjs +21 -24
  106. package/engine/pdf-build.mjs +217 -55
  107. package/engine/pdf-fonts.mjs +2 -2
  108. package/engine/pdf-images.mjs +82 -0
  109. package/engine/pdf-render.mjs +174 -22
  110. package/engine/populations.mjs +3 -2
  111. package/engine/project-init.mjs +499 -0
  112. package/engine/prose-config.mjs +1 -1
  113. package/engine/prose-lint.mjs +7 -6
  114. package/engine/readability-lint.mjs +86 -0
  115. package/engine/reckoning-markers.mjs +205 -93
  116. package/engine/scenes.mjs +5 -2
  117. package/engine/schema-check.mjs +3 -3
  118. package/engine/site-build.mjs +87 -37
  119. package/engine/site-config.mjs +8 -8
  120. package/engine/site-index.mjs +4 -0
  121. package/engine/site-maps.mjs +1 -1
  122. package/engine/site-preview.mjs +19 -8
  123. package/engine/social-ties.mjs +5 -1
  124. package/engine/sql-tables.mjs +92 -22
  125. package/engine/web-wikilinks.mjs +15 -31
  126. package/engine/wikilink-syntax.mjs +4 -1
  127. package/engine/wikilinks.mjs +12 -14
  128. package/hm3/item-fields.mjs +1 -1
  129. package/hm3/items.mjs +12 -45
  130. package/index.mjs +2 -14
  131. package/manifest.mjs +31 -4
  132. package/markdownlint-config.mjs +1 -1
  133. package/package.json +32 -11
  134. package/prettier-config.mjs +1 -1
  135. package/sohl/being-info.mjs +3 -145
  136. package/sohl/index.mjs +1 -1
  137. package/sohl/infobox.mjs +2 -1
  138. package/sohl/item-builders.mjs +3 -27
  139. package/sohl/item-fields.mjs +61 -22
  140. package/sohl/kb-passes.mjs +2 -2
  141. package/sohl/note-schemas.mjs +0 -8
  142. package/stage.mjs +19 -0
  143. package/types/config.d.mts +12 -3
  144. package/types/content-config.d.mts +15 -81
  145. package/types/engine/address-render.d.mts +13 -0
  146. package/types/engine/address.d.mts +2 -11
  147. package/types/engine/anchors.d.mts +3 -4
  148. package/types/engine/asset-bindings.d.mts +9 -2
  149. package/types/engine/base-compiler.d.mts +1 -2
  150. package/types/engine/being-measurements.d.mts +16 -0
  151. package/types/engine/calendar-choice.d.mts +14 -0
  152. package/types/engine/calendar-human.d.mts +4 -0
  153. package/types/engine/calendar-notes.d.mts +2 -0
  154. package/types/engine/calendars.d.mts +5 -3
  155. package/types/engine/code-fences.d.mts +6 -72
  156. package/types/engine/compile-corpus.d.mts +1 -3
  157. package/types/engine/content-admonitions.d.mts +24 -0
  158. package/types/engine/content-captions.d.mts +45 -0
  159. package/types/engine/content-footnotes.d.mts +10 -0
  160. package/types/engine/content-format-check.d.mts +17 -4
  161. package/types/engine/content-format.d.mts +24 -11
  162. package/types/engine/content-icons.d.mts +14 -23
  163. package/types/engine/content-images.d.mts +2 -2
  164. package/types/engine/content-index.d.mts +9 -2
  165. package/types/engine/content-links.d.mts +3 -5
  166. package/types/engine/content-tables.d.mts +11 -140
  167. package/types/engine/culture-choice.d.mts +14 -0
  168. package/types/engine/date-conversion.d.mts +4 -0
  169. package/types/engine/declaration-check.d.mts +24 -0
  170. package/types/engine/docs-checks.d.mts +28 -0
  171. package/types/engine/documentation-links.d.mts +7 -0
  172. package/types/engine/exported-scene.d.mts +6 -2
  173. package/types/engine/extension-attributes.d.mts +14 -0
  174. package/types/engine/field-spec.d.mts +9 -0
  175. package/types/engine/format-generated.d.mts +7 -0
  176. package/types/engine/frontmatter.d.mts +14 -4
  177. package/types/engine/generate.d.mts +1 -1
  178. package/types/engine/helpers.d.mts +37 -92
  179. package/types/engine/homepage.d.mts +2 -3
  180. package/types/engine/index.d.mts +5 -0
  181. package/types/engine/infobox-registry.d.mts +3 -1
  182. package/types/engine/infobox.d.mts +17 -6
  183. package/types/engine/item-compiler.d.mts +4 -4
  184. package/types/engine/item-docs.d.mts +4 -3
  185. package/types/engine/journals.d.mts +10 -7
  186. package/types/engine/map-graphviz.d.mts +1 -1
  187. package/types/engine/markdown-expressions.d.mts +17 -0
  188. package/types/engine/note-dates.d.mts +47 -1
  189. package/types/engine/note-package.d.mts +3 -19
  190. package/types/engine/note-state.d.mts +2 -1
  191. package/types/engine/note-vocabulary.d.mts +9 -0
  192. package/types/engine/number-words.d.mts +4 -0
  193. package/types/engine/office-holders.d.mts +14 -0
  194. package/types/engine/pack-config.d.mts +1 -1
  195. package/types/engine/packages.d.mts +3 -2
  196. package/types/engine/pdf-images.d.mts +38 -0
  197. package/types/engine/populations.d.mts +2 -2
  198. package/types/engine/project-init.d.mts +17 -0
  199. package/types/engine/prose-config.d.mts +1 -1
  200. package/types/engine/prose-lint.d.mts +3 -2
  201. package/types/engine/readability-lint.d.mts +22 -0
  202. package/types/engine/reckoning-markers.d.mts +4 -2
  203. package/types/engine/schema-check.d.mts +3 -3
  204. package/types/engine/site-build.d.mts +38 -28
  205. package/types/engine/site-config.d.mts +2 -2
  206. package/types/engine/site-index.d.mts +4 -0
  207. package/types/engine/site-preview.d.mts +1 -1
  208. package/types/engine/social-ties.d.mts +8 -2
  209. package/types/engine/sql-tables.d.mts +13 -2
  210. package/types/engine/web-wikilinks.d.mts +2 -1
  211. package/types/engine/wikilinks.d.mts +7 -1
  212. package/types/hm3/items.d.mts +5 -6
  213. package/types/labels.d.mts +40 -0
  214. package/types/manifest.d.mts +3 -3
  215. package/types/sohl/actors.d.mts +2 -2
  216. package/types/sohl/being-info.d.mts +3 -43
  217. package/types/sohl/index.d.mts +1 -1
  218. package/types/sohl/infobox.d.mts +5 -2
  219. package/types/sohl/item-fields.d.mts +4 -13
  220. package/types/stage.d.mts +9 -0
  221. package/CONTENT.md +0 -3239
package/README.md CHANGED
@@ -1,687 +1,48 @@
1
1
  # @heroiclands/package-build
2
2
 
3
- The shared toolchain for building and shipping a HeroicLands **Foundry
4
- package** — its compendium content and the parts Foundry loads besides.
3
+ The shared toolchain for building HeroicLands Foundry systems and modules. It compiles Markdown content into a searchable index, Foundry compendiums, website pages, and a book; it also assembles manifests, assets, bundles, and release archives.
5
4
 
6
- It has two halves, split by **input**:
5
+ ## Start here
7
6
 
8
- | Half | Reads | Produces |
9
- | ------------- | ------------------------------------------------------------ | -------------------------------------------------------------- |
10
- | **content** | `assets/content/**` | compendium packs, site content, content index |
11
- | **packaging** | `lang/`, `styles/`, `src/`, `assets/`, the manifest template | `system.json` / `module.json`, styles, bundle, release archive |
7
+ Use Node 24 or newer and npm. Initialize a project before it has a `package.json`, or add the package to an existing repository:
12
8
 
13
- The content half is documented separately in **[CONTENT.md](CONTENT.md)** — the
14
- note format, the pack pipeline, and the configuration contract a content tree
15
- declares itself with.
16
-
17
- Editor navigation for content projects is provided by
18
- [`@heroiclands/content-language-server`](https://github.com/HeroicLands/content-language-server).
19
-
20
- ## Install
21
-
22
- ```
23
- npm install -D @heroiclands/package-build
24
- ```
25
-
26
- ## What it covers
27
-
28
- The whole of assemble → validate → ship, one subpath each:
29
-
30
- - **`manifest`** — the Foundry package manifest, `system.json` or `module.json`:
31
- read the repository's template, stamp the version and the four release
32
- addresses, write it into the stage. The artifact is inferred from the
33
- template's name, and every address is derived from `package.json`'s
34
- `repository` — nothing is transcribed.
35
- - **`stage`** — assembling the build stage and clearing it away again. A listed
36
- asset path that does not exist **fails the build** rather than shipping a
37
- package that quietly lacks its localization or its templates, and the whole
38
- list is checked before anything is copied, so a bad list leaves no
39
- half-populated stage.
40
- - **`lang`** — what a shippable Foundry localization file must satisfy: it
41
- parses, its top level is an object, no key is both a leaf and a dotted prefix
42
- of another, placeholders are single-braced, and key segments carry no data.
43
- - **`coverage`** — whether the keys a package references and the keys it
44
- declares are the same set. A referenced key that is missing renders to a
45
- player as its own raw key string, so it fails the run; a declared key nothing
46
- references is reported and does not.
47
- - **`templates`** — the opposite question, which coverage cannot ask: whether
48
- the markup's user-visible text goes through localization at all, rather than
49
- sitting in the file in English — and whether each template still compiles once
50
- it does.
51
- - **`bundle`** — whether the manifest agrees with the file it points at.
52
- Declared under `"esmodules"` the bundle must parse as a module; declared under
53
- `"scripts"` it must declare **nothing** at top level, because every top-level
54
- declaration in a classic script is a global lexical binding and one colliding
55
- with a non-configurable `window` property throws at parse time.
56
- - **`release`** — the two assets a GitHub Release carries, `<artifact>.zip` and
57
- the manifest beside it. Waits for the archive to be _written_, not merely
58
- finalized.
59
- - **`deploy`** — installing a staged package into a Foundry data directory, over
60
- a local copy or SFTP. Always a staged, atomic swap: a running Foundry holds
61
- its LevelDB packs open, and replacing them in place leaves a directory LevelDB
62
- "repairs" to zero.
63
- - **`container`** — running what was just deployed. The Foundry data root a
64
- deploy writes into is the directory a container mounts, so serving the result
65
- is the next step from one variable rather than a second configuration.
66
- - **`e2e`** — a disposable world, seeded with a Gamemaster whose password is
67
- known, served until it is genuinely **active**, and a suite driven against it.
68
- What the suite _is_ stays the repository's.
69
- - **`text`** — locating a literal inside a file, so a finding names the line and
70
- column it is about.
71
-
72
- ## Configure
73
-
74
- A repository declares its build in **one** file — `package-build.config.yaml`.
75
- The content half reads its top level; the packaging half takes its settings from
76
- the `packageBuild:` section:
77
-
78
- ```yaml
79
- # Read from the top level, not restated below.
80
- packageKind: modules
81
- foundryPackage: sohl-thalorna
82
-
83
- packageBuild:
84
- # Where the package is assembled. Every `to:` below is relative to it, so a
85
- # table reads `lang`, not `build/stage/lang`.
86
- stageDir: build/stage
87
-
88
- assets:
89
- - { from: lang, to: lang }
90
- - { from: assets/icons, to: assets/icons }
91
- - { from: LICENSE.md, to: LICENSE.md }
92
-
93
- # Optional. A module exporting `transform(sourcePath) -> string | null`,
94
- # applied to every staged file — `null` copies it verbatim. This is the one
95
- # genuine piece of code in staging, and it stays the repository's: SoHL
96
- # rewrites each SVG's hard-coded fill so icons follow the Foundry theme.
97
- assetTransform: ./utils/svg-theme.mjs
98
-
99
- clean:
100
- # Beyond the conventional build artifacts, which the library already knows.
101
- extra: [coverage]
102
-
103
- lang:
104
- sources: lang/*.json
105
- # Printed after a `lang check` failure — where this repository documents its
106
- # key rules.
107
- help: See kb/dev-docs/reference/localization-keys.md.
108
-
109
- # `lang coverage` and `lang hardcoded`. Every one of these defaults to the
110
- # conventional layout, so a repository that follows it states none of them.
111
- primary: lang/en.json # the file coverage is measured against
112
- scripts: src/**/*.{ts,mjs} # scanned for key references
113
- templates: templates/**/*.hbs # scanned for references and for English
114
-
115
- # Only when the package references a root its file does not yet declare;
116
- # otherwise the roots are read from the file's own keys.
117
- keyRoots: [SOHL, TYPES, TYPE]
118
-
119
- # Optional. A module exporting `references(context) -> ReferenceSet`,
120
- # contributing the keys only this repository's conventions can find. SoHL's
121
- # `defineType(prefix, def)` mints one key per member of an enum by a rule of
122
- # its own; no shared guard can know it, and no repository can compare the
123
- # result against the file. See "Contributing generated keys" below.
124
- references: ./utils/lang-references.mjs
125
-
126
- # Keys reached in a way no scan can see, exempt from the unreferenced
127
- # advisory. The reason is required: it is a claim a reviewer has to be able
128
- # to check, and the honest fix for an unreferenced key is to delete it.
129
- retained:
130
- - prefix: SOHL.Gear.Action.
131
- reason: Titles built as `${titlePrefix}.${shortcode}` — the prefix is a parameter.
132
-
133
- # Template literals that are deliberately not localization keys. The escape
134
- # hatch, not the rule.
135
- allow:
136
- - literal: item.system.code === 'pyrn'
137
- reason: An expression example shown as a placeholder — code, not prose.
138
-
139
- deploy:
140
- # Prefix of the shared SFTP override variables. Default `SOHL`.
141
- envPrefix: SOHL
142
-
143
- bundle:
144
- # The file Foundry loads, as the manifest spells it, relative to the stage.
145
- # Defaults to `<packageId>.mjs`; state it only when the bundler emits
146
- # something else. Deliberately not read back out of the manifest — a value
147
- # taken from there would agree with itself by construction, and `bundle
148
- # check` asks whether the manifest declares this file correctly.
149
- entry: sohl.mjs
150
-
151
- # Optional. A module exporting `flags(config)` returning namespaced Foundry
152
- # flags the repository has to *compute* — an address that only exists once
153
- # the content tree has been walked, say. Merged over any declared below.
154
- manifestFlags: ./utils/manifest-flags.mjs
155
-
156
- # The Foundry package manifest. Emitted as declared, so a key Foundry adds in
157
- # a later version needs no release of this package.
158
- manifest:
159
- title: Song of Heroic Lands
160
- description: <p>…</p>
161
- license: LICENSE.md
162
- readme: README.md
163
- authors:
164
- - { name: Toasty, discord: "toasty#8538" }
165
- esmodules: [sohl.js]
166
- styles: [css/sohl.css]
167
- languages:
168
- - { lang: en, name: English, path: lang/en.json }
169
- documentTypes:
170
- Item:
171
- skill: { htmlFields: [notes, docHtml] }
172
- packFolders:
173
- - name: Song of Heroic Lands
174
- sorting: m
175
- color: "#094fcb"
176
- packs: [items, journals, actors, macros, scenes, adventures]
177
- media:
178
- - { type: logo, url: systems/sohl/assets/ui/logo.webp }
179
- socket: true
180
- grid: { distance: 5, units: ft }
181
- primaryTokenAttribute: health
182
-
183
- # Running the deployed package in a Foundry container. The four conventional
184
- # stages — dev, qa, prod, test — need no entry at all: their data root is
185
- # `FOUNDRYVTT_<STAGE>_DATA`, the same variable `deploy` writes into, and their
186
- # ports are conventional. Declare a stage only when it is genuinely yours.
187
- container:
188
- # Optional. The container name, minus the stage, which is always appended:
189
- # this declares `heroiclands-foundry-test`, not `sohl-foundry-test`.
190
- # Foundry binds a signed licence to the container hostname, so packages that
191
- # declare the same name are one instance covered by one signature. Defaults
192
- # to the package id, which keeps every package in its own container.
193
- name: heroiclands-foundry
194
-
195
- stages:
196
- # SoHL keeps the previous, pre-TypeScript system on an older Foundry.
197
- # An empty `world` declares "never auto-launch" — it is managed by hand.
198
- leg: { port: 30000, world: "", version: "12.331" }
199
-
200
- e2e:
201
- # The one thing the harness does not own. Standing Foundry up and seeding a
202
- # world is nobody's local problem; what runs against it is entirely yours.
203
- suite:
204
- run: [npx, cypress, run]
205
- open: [npx, cypress, open]
206
-
207
- # Where that suite writes its results. Optional, and what it buys is the
208
- # difference between "the suite ran and passed" and "the suite did not
209
- # run" — a headless run that writes nothing here is reported as a failure.
210
- results: [cypress/results]
211
-
212
- # What the fast loop can rebuild, in the order it must be built: the
213
- # bundler empties the stage, so it goes first. `recreate` marks a target
214
- # whose output Foundry reads once, at world launch.
215
- build:
216
- code: build:code
217
- assets: build:assets
218
- db: build:db
219
- system: { script: build:system, recreate: true }
220
-
221
- # Everything here is optional — it derives from the package id otherwise.
222
- world: { id: sohl-e2e, title: SoHL E2E }
223
- gm: { name: Gamemaster, password: sohl-e2e }
224
-
225
- # Extra world collections, compiled from directories of JSON documents.
226
- documents:
227
- actors: cypress/fixtures/actors
228
- ```
229
-
230
- ### The manifest is generated, not stamped
231
-
232
- `package-build manifest` writes `system.json` / `module.json` into the stage.
233
- **There is no template file.** A hand-authored JSON manifest is one the
234
- build stamped a few fields into — the one build input still written by hand, per
235
- repository, with no schema and nothing checking it. It also declared facts the
236
- configuration already declared: the pack list twice, in two formats, with
237
- nothing checking the pairs agreed.
238
-
239
- Three kinds of key end up in the result:
240
-
241
- | Kind | Where it comes from |
242
- | ------------ | ------------------------------------------------------------------------------------------------- |
243
- | **Declared** | `packageBuild.manifest`, emitted unchanged |
244
- | **Derived** | `id`, `version`, `url`, `bugs`, `manifest`, `download`, `compatibility`, `relationships`, `packs` |
245
- | **Computed** | namespaced `flags` from `manifestFlags`, merged over any declared |
246
-
247
- **Declaring a derived key is an error, not an override.** An authored `version`
248
- would look authoritative, sit there unread, and disagree with the shipped
249
- package forever; the build says so, naming the key and where the value actually
250
- comes from.
251
-
252
- `packs` is derived from the **one** pack list at the top level of
253
- `package-build.config.yaml` — each entry's `label`, `type`, `name` and
254
- `private`, plus a `system` from `stats.systemId` and a `path` of
255
- `packs/<name>`. Companions are flattened in, because Foundry sees no difference:
256
- a companion is only a pack written by another pass rather than one of its own.
257
- Give each pack the `label` you want Foundry to show.
258
-
259
- #### `packFolders` is checked against the packs you ship
260
-
261
- `packFolders` is the one **declared** key that names something the build
262
- **derives**. Every other declared key states a fact about the package (`title`,
263
- `socket`, `grid`) or addresses a staged file (`esmodules`, `styles`,
264
- `languages`) — a staged file being a different relation, answered against the
265
- stage rather than against configuration. So it is the one place a declaration
266
- can quietly go stale against a value the build already computed, and it did:
267
- `HarnMaster-3-FoundryVTT` shipped a folder naming four packs, three of which had
268
- not existed since its compendium was consolidated, while `items` — 1,577 of
269
- 1,597 documents — sat in no folder at all, with the build reporting nothing.
270
-
271
- `package-build manifest` now compares the two, descending through nested folders
272
- (Foundry allows three levels), and reports in the usual
273
- `file:line:column: severity: message` form:
274
-
275
- | Finding | Severity | Why |
276
- | ----------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
277
- | a folder names a pack the package does not ship | **error** | Foundry silently skips a name it cannot resolve, so the declaration does nothing; no arrangement intends it |
278
- | a pack no folder names | **warning** | legal, and a root-level pack can be deliberate — but a package that declared a folder rarely meant to leave one out |
279
- | no `packFolders` declared at all | nothing | everything at the root is an arrangement, not an omission |
280
-
281
- An error **stops the write**: a manifest already known to describe packs that do
282
- not exist should not reach the stage, where the next command would deploy it.
283
-
284
- ```text
285
- package-build.config.yaml:164:23: error: packFolders: folder "HârnMaster 3 System" names pack "character", which this package does not ship (packs: items, system-help)
286
- package-build.config.yaml:162:13: warning: packFolders: pack "items" is named by no folder, so it ships outside every folder this package declares
287
- ```
288
-
289
- `compatibility` and `relationships` are read from the **top level** of the
290
- shared configuration, not from this section — content-build consumes them
291
- (`supportedCoreVersion`, and a module'''s `stats.systemVersion`) and the
292
- dependency runs one way.
293
-
294
- **A relationship's build-only keys are not published.** `relationships` is the
295
- one block with a second reader, and content-build takes directives there:
296
- `itemCatalog: true` on a dependency selects that package's Item packs as a
297
- resolution source for the actors pass. That says how the package is _built_, not
298
- what it depends on, so it is dropped on the way into the manifest — Foundry's
299
- relationship schema does not define it, and someone reading a published manifest
300
- cannot tell a build directive from a declaration. Every other key is copied,
301
- including one this package has never heard of.
302
-
303
- **Why one file and not two.** Two of the values this package needs —
304
- `packageKind` and `foundryPackage` — are already declared for `content-build`. A
305
- second config file would restate them, which is two places for one fact; that is
306
- exactly what every consumer's `push-stage.mjs` did, hard-coding
307
- `packageKind: "systems"` and `packageId: "sohl"` beside a configuration that
308
- already said both.
309
-
310
- `content-build` checks only that `packageBuild:` is a mapping and hands it back
311
- frozen. Everything inside it is validated here, so neither package learns the
312
- other's schema — they split by input, and the dependency runs one way.
313
-
314
- **What is derived, not stated:**
315
-
316
- | Field | Derived from |
317
- | -------------------------- | --------------------------------------------------------------------------- |
318
- | the repository root | the configuration file's own location |
319
- | `packageKind`, `packageId` | the shared configuration's top level |
320
- | the release artifact | `packageKind` — a system ships `system.json`, a module `module.json` |
321
- | the bundle entry | `packageId` — `<id>.mjs`, unless `packageBuild.bundle.entry` says otherwise |
322
-
323
- **`packageKind: documentation` has no packaging half.** That kind publishes a
324
- website and a book and installs into no Foundry data directory, so there is no
325
- artifact to name and no id to derive one from: `manifest` refuses rather than
326
- writing a manifest for a package Foundry never installs, and the whole of such a
327
- package's build is `content-build`. See
328
- [A package that compiles nothing](CONTENT.md#a-package-that-compiles-nothing).
329
-
330
- ## Command line
331
-
332
- ```
333
- npx package-build clean [--distclean]
334
- npx package-build assets
335
- npx package-build manifest
336
- npx package-build lang check
337
- npx package-build lang coverage [--unused]
338
- npx package-build lang hardcoded
339
- npx package-build bundle check
340
- npx package-build release [--no-pdf]
341
- npx package-build site-root [--out <dir>]
342
- npx package-build deploy <stage>
343
- npx package-build container <stage> <start|stop|restart|recreate|rm|status|logs|pull>
344
- npx package-build e2e <seed|run|open|fast|sweep>
345
- ```
346
-
347
- `package-build release` writes `<artifact>.zip` and `<artifact>.json` into
348
- `build/dist`, the content index the manifest advertises, and — when the package
349
- publishes a content tree — the book that tree compiles to, as
350
- `<artifact>-<version>.pdf`.
351
-
352
- `package-build site-root` finishes a site build. Hugo renders into
353
- `build/site/<contentPackage>/`; this writes the deployment's `_headers` beside
354
- that, and indexes the rendered pages for search with Pagefind into
355
- `build/site/<contentPackage>/pagefind/`, deployed with the pages and served at
356
- `/<contentPackage>/pagefind/`. The dependency is this package's, declared once,
357
- so every site — and every local build — gets an index without the repository
358
- adding one; `site.search: false` in `package-build.config.yaml` turns it off.
359
-
360
- The book is **built by default and never fatal**. A package publishing only a
361
- homepage, one with no `pdf:` block, and one with no content tree each build
362
- none and say so; `--no-pdf` is for a release that has a tree and does not want
363
- the book. What the book is and how it is configured is `CONTENT.md`, under
364
- _Publishing a book_ — it is a content surface, and the content half owns it.
365
-
366
- Wrapped as npm scripts — SoHL spells them:
367
-
368
- ```json
369
- {
370
- "clean": "package-build clean",
371
- "distclean": "package-build clean --distclean",
372
- "build:assets": "package-build assets",
373
- "lint:lang": "package-build lang check",
374
- "lint:lang-coverage": "package-build lang coverage",
375
- "lint:lang-hardcoded": "package-build lang hardcoded",
376
- "lint:bundle-globals": "package-build bundle check",
377
- "build:pack-release": "package-build release",
378
- "push:qa": "package-build deploy qa",
379
- "container:dev": "package-build container dev",
380
- "e2e:full": "package-build e2e run",
381
- "e2e:fast": "package-build e2e fast",
382
- "e2e:sweep": "package-build e2e sweep"
383
- }
384
- ```
385
-
386
- **The shape of the surface.** A capability with a single operation is a bare
387
- command (`clean`, `assets`, `manifest`, `release`, `deploy <stage>`); one with
388
- more than a single operation takes a positional action, so a second can be added
389
- without renaming the first (`lang check`, `bundle check`). Flat `lang:check`
390
- names would make every operation a new top-level command and hide which ones
391
- belong together.
392
-
393
- **What the two localization guards ask.** They are deliberate opposites, and
394
- neither can answer the other's question. `lang coverage` walks _key → file_, so
395
- it is completely blind to a template that names no key whatsoever; `lang
396
- hardcoded` walks _text → key_, and catches exactly that. Before the work that
397
- prompted the second, Song of Heroic Lands had **516 hardcoded English literals
398
- across 61 templates** — translating every key in `en.json` would have left every
399
- one of them in English.
400
-
401
- **Contributing generated keys.** `packageBuild.lang.references` names a module
402
- exporting `references(context)`, called once with
403
- `{ config, rootDir, roots, files }` — `files` being the scanned sources as
404
- `{ path, text }`, already read, so the contributor sees exactly the text the
405
- built-in scan saw. It returns a reference set:
406
-
407
- ```js
408
- export function references({ files }) {
409
- return {
410
- keys: [
411
- // `exact` when the key is minted whole, so keys sitting beneath it do
412
- // not vouch for it; `origin` is the verb phrase the message reads with.
413
- {
414
- key: "SOHL.Skill.CODE.lore",
415
- file: "src/utils/constants.ts",
416
- line: 42,
417
- exact: true,
418
- origin: "defineType generates",
419
- },
420
- ],
421
- namespaces: ["SOHL.Skill.CODE"], // families whose leaves are never named
422
- patterns: ["SOHL.Skill.*.label"], // shapes, `*` standing for one segment
423
- findings: [], // whatever the contributor could not resolve
424
- };
425
- }
9
+ ```bash
10
+ npm exec --package=@heroiclands/package-build -- package-build init my-package --kind modules --name my-package --title "My Package" --description "A content package." --author "Your Name" --license original
426
11
  ```
427
12
 
428
- Everything Foundry-shaped is already built in — `{{localize}}`,
429
- `game.i18n.localize` / `format`, keys in string and template literals, a
430
- DataModel's `LOCALIZATION_PREFIXES`, the `FIELDS.<field>.label` / `.hint` keys Foundry
431
- mints off one. A contributor is for what only the repository knows.
432
-
433
- **What `bundle check` checks.** Three ways a package builds successfully and
434
- still does not load, none of which a bundler can see, because each is a
435
- disagreement between two files rather than a fault in either:
436
-
437
- | The manifest says | What Foundry does |
438
- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
439
- | the entry under both `esmodules` and `scripts` | loads the bundle twice |
440
- | the entry under neither | never loads it at all |
441
- | the entry under `esmodules`, but the file only parses as a classic script | fails at load, naming whichever `import` came first and nothing about the manifest |
442
-
443
- It reads both files from the stage, because the stage is what ships — checking
444
- sources would answer for a package nobody installs. Findings are emitted as
445
- `file:line:column: severity: message`, and the command exits non-zero when any
446
- of them is an error.
447
-
448
- **Why the CLI exists.** This package was library-only, so every consuming
449
- repository wrote a wrapper script per job — six of them in the SoHL repository,
450
- 441 lines that between them contained no logic. `clean.mjs` was 47 lines that
451
- computed a `repoRoot` from `import.meta.url`, read one flag, and made one call.
452
- Every copy had drifted from its sibling in the other repositories, because
453
- copies do: `clean.mjs` was 47 lines in one and 48 in another, `copy-assets.mjs`
454
- 71 and 76.
455
-
456
- It is the same shape the configuration had before it became data — not logic,
457
- but the boilerplate a code file needs in order to state a literal. The literals
458
- moved into configuration; the boilerplate lives in the CLI, once.
459
-
460
- `--version` and `--help` answer in a directory with no configuration at all.
461
- Running an actual command resolves it, and fails loudly when it is missing.
462
-
463
- ## Standing Foundry up, and running a suite against it
464
-
465
- A package that claims a `compatibility` range is making a promise, and a
466
- promise is only defended if something exercises it. Until now exactly one
467
- repository could: the container lifecycle, the world seeding and the browser
468
- harness all lived in `SoHL/utils/`, so two of the three HeroicLands module
469
- repositories declared a range nothing could test.
470
-
471
- **The seam already existed.** `package-build deploy <stage>` installs a staged
472
- package into `FOUNDRYVTT_<STAGE>_DATA`; a container mounts that same directory
473
- at `/data` and serves it. Running Foundry against what was just deployed is the
474
- next step from one variable, not a second configuration — so nothing about the
475
- destination, the stage, or the package identity is restated.
476
-
477
- ### Which Foundry build a run pins
478
-
479
- `compatibility.minimum` is the oldest Foundry a package claims to run on, and
480
- that claim is what the suite exists to defend. Testing above the floor tests a
481
- configuration no user is promised while leaving the promised one unverified: a
482
- regression that breaks the floor but works on a newer build passes in silence.
483
-
484
- So **the end-to-end stage is pinned to `compatibility.minimum` itself**, read
485
- from the top level of the shared configuration. The claim and the evidence are
486
- the same number and cannot drift apart, and raising the pin is what it should
487
- be — a decision to raise the supported floor, made by editing the claim.
488
-
489
- `FOUNDRYVTT_<STAGE>_VERSION` still wins, so a contributor can sit on another
490
- build without touching committed configuration. A floor that names no build
491
- (`"14"`) cannot pin one; the run floats on the major tag, visibly, rather than
492
- pretending to a precision it lacks.
493
-
494
- ### The sweep
495
-
496
- Routine runs go against the floor, which leaves the other direction untested: a
497
- new Foundry release can break a package and nothing notices until a user does.
498
-
499
- ```
500
- npx package-build e2e sweep 14.367
501
- ```
502
-
503
- That is the full suite — reseeded world and all — against a build the
504
- repository does not pin. It must be the full path rather than the fast one: a
505
- seeded world is stamped with the build that created it, and Foundry refuses to
506
- auto-launch a world stamped by another. It takes the build as an argument and
507
- has **no default**, because the product of a sweep is a citable result ("the
508
- full suite passed on 14.367") and "the newest release" is not a constant any
509
- repository can hold without rotting.
510
-
511
- A green sweep is what licenses moving `compatibility.verified` to that build. A
512
- red one is the early warning the sweep exists to produce.
513
-
514
- ### A run that executed nothing is not a pass
515
-
516
- Because a sweep is evidence, the exit status of the suite is not enough on its
517
- own to call a run green. Every way of stopping a runner before it starts — a
518
- corrupt install, a missing browser, a killed process, an `npm ci` racing the
519
- run and taking `node_modules` with it — produces a run that executed nothing,
520
- and a harness that reports that as 0 makes the evidence unfalsifiable in the
521
- one direction that matters.
522
-
523
- So the suite is bracketed rather than trusted:
524
-
525
- | Check | What it catches |
526
- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
527
- | Before, the command resolves | The runner is not installed. The error names it, before a container is stood up and a world seeded. |
528
- | A runner is read through | `npx cypress run` is checked for **`cypress`**, not `npx` — `npx` is never missing, so it answers nothing. |
529
- | After, the command still does | The runner went away mid-run. That is a run that cannot have finished, whatever it exited with. |
530
- | After, results are newer | The suite started and produced nothing. Requires `packageBuild.e2e.results`. |
531
-
532
- `results` names one path or a list of them, relative to the repository root.
533
- Existence is not the test — a directory the _previous_ run left behind exists,
534
- and reading that as evidence would make the check agree with exactly what it
535
- was built to catch. What counts is a file modified since the spawn.
536
-
537
- Two things it deliberately does not do. It only ever makes a verdict **worse**:
538
- a suite that failed keeps its own status, so there is no second path by which
539
- the harness can report a result that did not happen. And it does not apply to
540
- `open`, where a person decides what to execute and when to close the runner —
541
- "it wrote no results" describes that session rather than faulting it.
542
-
543
- ### Waiting for a world, not for a port
544
-
545
- Foundry answers on its port long before a world is serving. A suite started at
546
- that moment fails every spec for no visible reason, which is the single most
547
- expensive way an integration harness wastes an afternoon. So the wait is for
548
- the join screen — the form a world renders only once it is **active** — and a
549
- licence failure, which never recovers, is read out of the container log and
550
- reported at once rather than after a three-minute timeout that explains
551
- nothing.
552
-
553
- ### The fast loop
554
-
555
- ```
556
- npx package-build e2e fast -- --spec cypress/e2e/skill.cy.js
557
- ```
558
-
559
- Rebuild what changed, redeploy, cycle the world, wait for it, re-run. It is one
560
- command because every step of it has a quiet failure mode:
561
-
562
- | Step | What goes wrong by hand |
563
- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------- |
564
- | Build | The container serves the **built** package, not the source tree. |
565
- | Order | A bundler empties the stage, so building it after the asset and pack passes silently discards them — and the deploy is a mirror. |
566
- | Manifest | Read once at world launch, so it needs a container **recreate**, not a restart. A target declaring `recreate: true` forces one. |
567
- | Restart | A running Foundry holds its packs open, so a content change is invisible until the world reopens them. |
568
- | Stale lock | A container that died holding the data-root lock makes every later boot fail with a message that names no owner. |
569
- | Readiness | `docker start` returns long before Foundry serves. |
570
- | The suite | Editor and agent shells export `ELECTRON_RUN_AS_NODE`, which makes an Electron runner start as plain Node and die on its own flags. |
571
-
572
- `--build` takes a comma-separated list of declared targets, `all`, or `none`;
573
- an unrecognised one fails fast rather than half-deploying. `--recreate` forces
574
- a recreate, `--no-run` stops once the environment is current, and everything
575
- else — including anything after a bare `--` — is handed to the suite verbatim.
576
-
577
- ### What the seeded world holds
578
-
579
- A world Foundry will launch without a migration prompt: `world.json`, and one
580
- Gamemaster whose password is known, so a spec can log in deterministically. It
581
- also carries one **active** scene, because an empty world auto-starts Foundry's
582
- welcome tour (whose callout overlays sheets) and a world with no active scene
583
- has no ready canvas.
584
-
585
- For a **module** package it additionally writes `core.moduleConfiguration`,
586
- switching the module on. A system is the world's own and `world.json` names it;
587
- a module is not, and without that setting a module repository would stand its
588
- suite up against a world that never loaded the thing under test.
589
-
590
- Anything else — which actors, which journals — is the repository's, declared as
591
- `packageBuild.e2e.documents` and compiled in beside the built-ins.
592
-
593
- The world directory is wiped and rewritten on every seed. That is what makes a
594
- run repeatable, and it is why `FOUNDRYVTT_<E2E_STAGE>_DATA` must be a separate,
595
- empty directory: pointing it at a working stage would let the seed delete
596
- worlds there, and would make the image reuse that stage's `Config/license.json`
597
- in place of the key dedicated to the suite.
598
-
599
- ## Design
600
-
601
- **The rules are pure, and I/O is confined to functions named for it.** A rule
602
- takes source text or data and returns findings or values; discovery and
603
- reporting stay with the caller. Where a step genuinely has to touch disk or a
604
- network it is an export named for what it does — `writeFoundryManifest`,
605
- `stageAssets`, `packRelease`, `deployStage`.
606
-
607
- That is what lets one rule set serve a `lint` script, a build step and a unit
608
- test without any of them agreeing on how files are found or how findings are
609
- printed — and it is what makes the rules testable at all, which the scripts they
610
- were extracted from were not: each ran its work at import time and exported
611
- nothing.
612
-
613
- Findings carry the fields the shared diagnostic format takes (`line`, `column`,
614
- `severity`, `message`) but never `file`, which only the caller knows. The format
615
- itself is owned by `@heroiclands/content-build`'s `engine/diagnostics`, and is
616
- not restated here.
617
-
618
- ```js
619
- import { validateLangSource } from "@heroiclands/package-build/lang";
620
-
621
- for (const file of globSync("lang/*.json")) {
622
- for (const finding of validateLangSource(readFileSync(file, "utf8"))) {
623
- reportDiagnostic({ file, ...finding });
624
- }
625
- }
626
- ```
627
-
628
- ## Tests
629
-
630
- ```
631
- npm test
13
+ ```bash
14
+ npm install -D @heroiclands/package-build
632
15
  ```
633
16
 
634
- Plain `vitest`, no setup file and no aliases: everything here is ESM over Node
635
- built-ins and what `dependencies` declares, and a harness that offered a Foundry
636
- global would let something reach for one. (This said "three dependencies" while
637
- it described the packaging half alone; absorbing the content half at 3.0.0
638
- brought the compendium, Markdown and SQL toolchains with it. The number is left
639
- out rather than restated, because the next dependency would make a restated one
640
- wrong again.)
641
-
642
- `tests/dependencies-are-declared.test.ts` is the guard an extraction most needs
643
- — every bare specifier in a shipped file must be a builtin, this package, or a
644
- declared `dependency`. Inside a workspace, which this package was for its first
645
- six changes, a missing declaration is invisible; installed from npm it fails on
646
- the first import.
647
-
648
- ## Releasing
649
-
650
- Releasing is not a command anyone runs. It is a consequence of merging, in two
651
- steps, and each step is visible while it is pending.
652
-
653
- **A pull request declares its bump when a consumer will notice.** Run
654
- `npx changeset` and pick major/minor/patch; the summary you write becomes the
655
- changelog entry and the release note. Nothing a consumer meets means no
656
- changeset at all — CI carries no gate demanding one. What CI does check is the
657
- quality of a changeset that exists: `node bin/package-build.mjs changelog check`
658
- lints every pending one against the rules a changeset is held to.
659
-
660
- **Merging to `main` opens a Version Packages pull request** carrying the version
661
- bump and the rewritten `CHANGELOG.md`. That pull request _is_ the pending
662
- release: as long as something is merged but unpublished, there is an open pull
663
- request saying so. The previous, hand-driven process failed by leaving _nothing_
664
- behind when its final step was forgotten (#4).
665
-
666
- **Merging that publishes.** `changeset publish` puts the version on npm through
667
- Trusted Publishing (OIDC — there is no `NPM_TOKEN`), tags the commit `v<version>`
668
- and cuts the GitHub Release with the changelog section as its body. It publishes
669
- only versions that are not already on the registry, so re-running it is a no-op;
670
- `workflow_dispatch` on **Publish to npm** is the recovery path if a run fails
671
- after versioning.
672
-
673
- A changeset is also where a **raised dependency floor** gets recorded. 0.2.0 began
674
- requiring `@heroiclands/content-build >= 0.15.0` and announced it nowhere; a
675
- changeset is the place that now happens.
676
-
677
- Below 1.0.0, `^0.x` never crosses a minor — a consumer on `^0.2.0` will not see
678
- `0.3.0` until it bumps the pin deliberately, and Dependabot raises that as its own
679
- pull request.
680
-
681
- > After a successful publish, `npm view @heroiclands/package-build version` can
682
- > report the _previous_ version for a minute or so. `dist-tags` is correct
683
- > immediately, and is what the workflow prints.
684
-
685
- ## Licence
686
-
687
- GPL-3.0-or-later.
17
+ The generated project includes a homepage, website and book targets, npm scripts, and a local `.gitignore`. [Getting started](docs/getting-started.md) routes you to a [system](docs/guides/system-project.md), [module](docs/guides/module-project.md), or [content authoring](docs/authoring/first-note.md) path. The [content-language-server](https://github.com/HeroicLands/content-language-server) provides editor navigation and completion.
18
+
19
+ ## Documentation
20
+
21
+ The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md).
22
+
23
+ | Topic | Guide |
24
+ | ------------------------------------------------- | ------------------------------------------------------------ |
25
+ | Documentation landing | [Home](docs/index.md) |
26
+ | Project layout and scripts | [Project setup](docs/project-setup.md) |
27
+ | Note model and output | [Content format](docs/content-format.md) |
28
+ | Frontmatter and system blocks | [Frontmatter](docs/authoring/frontmatter.md) |
29
+ | Addresses, links, anchors, SQL, and secret blocks | [Links and markup](docs/authoring/links-and-markup.md) |
30
+ | Images, icons, audio, and provenance | [Assets](docs/authoring/assets.md) |
31
+ | Dates, calendars, and eras | [Dates and calendars](docs/authoring/dates-and-calendars.md) |
32
+ | Complete note fields and mappings | [Note types](docs/reference/note-types.md) |
33
+ | Detailed format rules and examples | [Format details](docs/reference/format-details.md) |
34
+ | Compendium routing and document builds | [Packs](docs/guides/packs.md) |
35
+ | Website build and publication | [Site](docs/guides/site.md) |
36
+ | PDF book build | [Book](docs/guides/book.md) |
37
+ | Icon generation and use | [Icons](docs/guides/icons.md) |
38
+ | Reference pages and content indexes | [Reference pages](docs/guides/reference-pages.md) |
39
+ | Localization and templates | [Localization](docs/guides/localization.md) |
40
+ | Foundry integration testing | [End-to-end testing](docs/guides/e2e.md) |
41
+ | Release and deployment | [Release and deploy](docs/guides/release-and-deploy.md) |
42
+ | Every CLI command | [Commands](docs/commands.md) |
43
+ | Every build setting | [Configuration](docs/configuration.md) |
44
+ | Programmatic exports | [API](docs/api.md) |
45
+ | Error locations and severities | [Diagnostics](docs/diagnostics.md) |
46
+ | Architectural rationale | [Design](docs/design.md) |
47
+
48
+ The package is GPL-3.0-or-later for code and CC-BY-SA-4.0 for original content. See [LICENSE.md](LICENSE.md).