@heroiclands/package-build 23.1.0 → 23.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/bin/content-commands.mjs +68 -149
  3. package/bin/package-build.mjs +56 -10
  4. package/ci/check-banners.mjs +42 -6
  5. package/ci/confirm-npm-version.mjs +135 -0
  6. package/ci/content-format-reference.mjs +14 -5
  7. package/config.mjs +1 -0
  8. package/container.mjs +185 -4
  9. package/content-config.mjs +333 -210
  10. package/deploy.mjs +116 -10
  11. package/docs/api.md +238 -246
  12. package/docs/authoring/assets.md +11 -5
  13. package/docs/authoring/frontmatter.md +1 -1
  14. package/docs/authoring/links-and-markup.md +87 -0
  15. package/docs/commands.md +138 -99
  16. package/docs/configuration.md +277 -125
  17. package/docs/content-format.md +2 -0
  18. package/docs/diagnostics.md +48 -13
  19. package/docs/guides/book.md +341 -21
  20. package/docs/guides/e2e.md +13 -2
  21. package/docs/guides/icons.md +14 -4
  22. package/docs/guides/module-project.md +18 -8
  23. package/docs/guides/release-and-deploy.md +54 -6
  24. package/docs/guides/site.md +5 -2
  25. package/docs/project-setup.md +30 -10
  26. package/docs/reference/format-details.md +373 -170
  27. package/docs/reference/note-types.md +128 -109
  28. package/docs/reference/theme-pages.md +31 -6
  29. package/e2e.mjs +140 -28
  30. package/engine/actor-compiler.mjs +22 -31
  31. package/engine/address-charset.mjs +4 -17
  32. package/engine/address-fields.mjs +1 -1
  33. package/engine/address-render.mjs +12 -1
  34. package/engine/address-values.mjs +6 -0
  35. package/engine/address.mjs +46 -33
  36. package/engine/art-fields.mjs +30 -5
  37. package/engine/art-slots.mjs +10 -1
  38. package/engine/asset-bindings.mjs +12 -1
  39. package/engine/base-compiler.mjs +34 -65
  40. package/engine/being-age.mjs +2 -7
  41. package/engine/being-measurements.mjs +12 -1
  42. package/engine/calendar-artifacts.mjs +6 -0
  43. package/engine/calendar-choice.mjs +12 -1
  44. package/engine/calendar-format.mjs +12 -1
  45. package/engine/calendar-human.mjs +12 -1
  46. package/engine/compile-corpus.mjs +3 -1
  47. package/engine/content-address.mjs +9 -12
  48. package/engine/content-alerts.mjs +13 -1
  49. package/engine/content-blocks.mjs +14 -3
  50. package/engine/content-embeds.mjs +11 -3
  51. package/engine/content-figures.mjs +9 -0
  52. package/engine/content-footnotes.mjs +12 -1
  53. package/engine/content-format-check.mjs +11 -262
  54. package/engine/content-format.yaml +5 -2
  55. package/engine/content-images.mjs +3 -0
  56. package/engine/content-index.mjs +443 -20
  57. package/engine/content-links.mjs +29 -35
  58. package/engine/content-lint.mjs +2 -3
  59. package/engine/content-poetry.mjs +12 -1
  60. package/engine/content-slug.mjs +2 -14
  61. package/engine/content-spans.mjs +13 -1
  62. package/engine/content-tables.mjs +12 -1
  63. package/engine/culture-choice.mjs +210 -13
  64. package/engine/data-keys.mjs +19 -16
  65. package/engine/date-conversion.mjs +12 -1
  66. package/engine/declaration-check.mjs +9 -0
  67. package/engine/derived-fields.mjs +6 -7
  68. package/engine/diagnostics.mjs +34 -4
  69. package/engine/docs-checks.mjs +9 -0
  70. package/engine/document-subtypes.mjs +47 -34
  71. package/engine/event-views.mjs +139 -14
  72. package/engine/exported-scene.mjs +92 -28
  73. package/engine/extension-attributes.mjs +12 -1
  74. package/engine/field-spec.mjs +11 -102
  75. package/engine/folder-notes.mjs +0 -64
  76. package/engine/foreign-catalog.mjs +2 -1
  77. package/engine/format-generated.mjs +9 -0
  78. package/engine/foundry-entries.mjs +10 -28
  79. package/engine/frontmatter-lint.mjs +83 -392
  80. package/engine/frontmatter.mjs +20 -70
  81. package/engine/generate.mjs +10 -15
  82. package/engine/heading-attributes.mjs +12 -1
  83. package/engine/helpers.mjs +41 -102
  84. package/engine/holdings.mjs +1 -1
  85. package/engine/homepage.mjs +3 -10
  86. package/engine/ids.mjs +1 -167
  87. package/engine/index-records.mjs +6 -4
  88. package/engine/index.mjs +0 -3
  89. package/engine/infobox-render.mjs +15 -3
  90. package/engine/infobox.mjs +13 -14
  91. package/engine/item-compiler.mjs +4 -10
  92. package/engine/item-docs.mjs +3 -5
  93. package/engine/item-registry.mjs +2 -10
  94. package/engine/journals.mjs +1 -5
  95. package/engine/literature-notes.mjs +4 -12
  96. package/engine/macros.mjs +1 -2
  97. package/engine/map-graphviz.mjs +12 -1
  98. package/engine/markdown-expressions.mjs +12 -1
  99. package/engine/markdownlint-run.mjs +567 -0
  100. package/engine/metadata-index.mjs +42 -12
  101. package/engine/note-addresses.mjs +143 -35
  102. package/engine/note-claims.mjs +14 -28
  103. package/engine/note-dates.mjs +3 -3
  104. package/engine/note-frontmatter.mjs +6 -0
  105. package/engine/note-ids.mjs +8 -14
  106. package/engine/note-package.mjs +8 -25
  107. package/engine/note-state.mjs +1 -3
  108. package/engine/note-vocabulary.mjs +148 -84
  109. package/engine/number-words.mjs +12 -1
  110. package/engine/pack-config.mjs +15 -25
  111. package/engine/pack-router.mjs +10 -16
  112. package/engine/pathnames.mjs +8 -22
  113. package/engine/pdf-build.mjs +773 -121
  114. package/engine/pdf-coverage.mjs +188 -0
  115. package/engine/pdf-index.mjs +54 -7
  116. package/engine/pdf-pages.mjs +247 -0
  117. package/engine/pdf-render.mjs +114 -4
  118. package/engine/pdf-toc.mjs +29 -32
  119. package/engine/project-init.mjs +46 -6
  120. package/engine/prose-config.mjs +1 -1
  121. package/engine/prose-lint.mjs +52 -88
  122. package/engine/readability-lint.mjs +9 -0
  123. package/engine/reckoning-markers.mjs +12 -1
  124. package/engine/reference-targets.mjs +7 -0
  125. package/engine/runtime-only-fields.mjs +5 -8
  126. package/engine/scene-levels.mjs +2 -2
  127. package/engine/scenes.mjs +1 -1
  128. package/engine/shortcode-references.mjs +7 -0
  129. package/engine/site-build.mjs +30 -16
  130. package/engine/site-index.mjs +10 -16
  131. package/engine/site-preview.mjs +6 -0
  132. package/engine/site-root.mjs +8 -1
  133. package/engine/social-tie-terms.mjs +12 -1
  134. package/engine/social-ties.mjs +3 -0
  135. package/engine/sql-tables.mjs +56 -20
  136. package/engine/standing-terms.mjs +11 -1
  137. package/engine/standings.mjs +11 -27
  138. package/engine/system-block.mjs +39 -141
  139. package/engine/system-vocabulary.mjs +6 -0
  140. package/engine/walk-files.mjs +69 -0
  141. package/engine/web-wikilinks.mjs +23 -3
  142. package/engine/wikilink-syntax.mjs +32 -21
  143. package/engine/wikilinks.mjs +27 -16
  144. package/engine/yaml-lint.mjs +7 -4
  145. package/githooks/attribution.mjs +8 -0
  146. package/githooks/commit-message.mjs +9 -0
  147. package/hm3/actors.mjs +9 -48
  148. package/hm3/default-item-art.mjs +9 -9
  149. package/hm3/item-fields.mjs +2 -9
  150. package/hm3/template-priority.mjs +6 -7
  151. package/hugo-theme/data/banner-kinds.yaml +78 -0
  152. package/hugo-theme/data/banners.yaml +2 -3
  153. package/hugo-theme/layouts/404.html +3 -3
  154. package/hugo-theme/layouts/partials/banner-url.html +31 -7
  155. package/hugo-theme/layouts/partials/hero-banner.html +13 -14
  156. package/hugo-theme/layouts/partials/related.html +1 -1
  157. package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
  158. package/hugo-theme/static/css/style.css +1 -1
  159. package/manifest.mjs +11 -4
  160. package/markdownlint-config.mjs +1 -1
  161. package/package.json +6 -3
  162. package/release.mjs +83 -18
  163. package/sohl/actors.mjs +16 -27
  164. package/sohl/default-item-art.mjs +3 -3
  165. package/sohl/document-subtypes.mjs +1 -16
  166. package/sohl/infobox.mjs +2 -3
  167. package/sohl/item-builders.mjs +5 -8
  168. package/sohl/item-fields.mjs +5 -35
  169. package/sohl/items.mjs +0 -4
  170. package/sohl/kb-passes.mjs +3 -3
  171. package/types/container.d.mts +116 -5
  172. package/types/content-config.d.mts +83 -35
  173. package/types/deploy.d.mts +42 -6
  174. package/types/e2e.d.mts +35 -3
  175. package/types/engine/address-charset.d.mts +4 -17
  176. package/types/engine/address.d.mts +17 -8
  177. package/types/engine/art-fields.d.mts +7 -4
  178. package/types/engine/art-slots.d.mts +6 -0
  179. package/types/engine/base-compiler.d.mts +15 -17
  180. package/types/engine/content-address.d.mts +9 -12
  181. package/types/engine/content-embeds.d.mts +4 -2
  182. package/types/engine/content-format-check.d.mts +0 -64
  183. package/types/engine/content-index.d.mts +82 -3
  184. package/types/engine/content-links.d.mts +2 -4
  185. package/types/engine/culture-choice.d.mts +51 -8
  186. package/types/engine/data-keys.d.mts +2 -2
  187. package/types/engine/derived-fields.d.mts +1 -2
  188. package/types/engine/diagnostics.d.mts +27 -4
  189. package/types/engine/document-subtypes.d.mts +12 -31
  190. package/types/engine/event-views.d.mts +14 -2
  191. package/types/engine/exported-scene.d.mts +3 -2
  192. package/types/engine/field-spec.d.mts +13 -122
  193. package/types/engine/folder-notes.d.mts +0 -39
  194. package/types/engine/foreign-catalog.d.mts +2 -1
  195. package/types/engine/foundry-entries.d.mts +4 -5
  196. package/types/engine/frontmatter-lint.d.mts +2 -2
  197. package/types/engine/frontmatter.d.mts +19 -45
  198. package/types/engine/generate.d.mts +1 -1
  199. package/types/engine/helpers.d.mts +25 -42
  200. package/types/engine/homepage.d.mts +3 -10
  201. package/types/engine/ids.d.mts +0 -114
  202. package/types/engine/index-records.d.mts +6 -4
  203. package/types/engine/index.d.mts +0 -1
  204. package/types/engine/infobox-render.d.mts +7 -1
  205. package/types/engine/infobox.d.mts +3 -2
  206. package/types/engine/item-docs.d.mts +1 -1
  207. package/types/engine/item-registry.d.mts +2 -4
  208. package/types/engine/literature-notes.d.mts +1 -1
  209. package/types/engine/markdownlint-run.d.mts +40 -0
  210. package/types/engine/metadata-index.d.mts +9 -1
  211. package/types/engine/note-addresses.d.mts +27 -2
  212. package/types/engine/note-claims.d.mts +3 -5
  213. package/types/engine/note-ids.d.mts +2 -4
  214. package/types/engine/note-package.d.mts +1 -1
  215. package/types/engine/note-state.d.mts +1 -3
  216. package/types/engine/note-vocabulary.d.mts +28 -21
  217. package/types/engine/pack-config.d.mts +2 -6
  218. package/types/engine/pack-router.d.mts +4 -4
  219. package/types/engine/pathnames.d.mts +3 -3
  220. package/types/engine/pdf-build.d.mts +98 -12
  221. package/types/engine/pdf-coverage.d.mts +60 -0
  222. package/types/engine/pdf-index.d.mts +34 -5
  223. package/types/engine/pdf-pages.d.mts +143 -0
  224. package/types/engine/pdf-render.d.mts +45 -1
  225. package/types/engine/pdf-toc.d.mts +12 -14
  226. package/types/engine/prose-config.d.mts +1 -1
  227. package/types/engine/prose-lint.d.mts +11 -10
  228. package/types/engine/runtime-only-fields.d.mts +2 -2
  229. package/types/engine/site-build.d.mts +9 -5
  230. package/types/engine/site-index.d.mts +4 -5
  231. package/types/engine/site-root.d.mts +1 -1
  232. package/types/engine/sql-tables.d.mts +31 -2
  233. package/types/engine/standing-terms.d.mts +2 -1
  234. package/types/engine/standings.d.mts +7 -9
  235. package/types/engine/system-block.d.mts +10 -44
  236. package/types/engine/walk-files.d.mts +15 -0
  237. package/types/engine/wikilink-syntax.d.mts +18 -19
  238. package/types/engine/yaml-lint.d.mts +5 -4
  239. package/types/hm3/actors.d.mts +1 -2
  240. package/types/hm3/item-fields.d.mts +2 -4
  241. package/types/hm3/template-priority.d.mts +6 -7
  242. package/types/manifest.d.mts +3 -2
  243. package/types/release.d.mts +12 -7
  244. package/types/sohl/actors.d.mts +2 -1
  245. package/types/sohl/document-subtypes.d.mts +1 -2
  246. package/types/sohl/item-fields.d.mts +3 -7
  247. package/engine/retired-fields.mjs +0 -631
  248. package/types/engine/retired-fields.d.mts +0 -354
@@ -51,9 +51,9 @@ import log from "loglevel";
51
51
  * code spans rather than refuse to build.
52
52
  *
53
53
  * **Configuring one that cannot be used is a defect, and it fails the build.**
54
- * A missing file, a malformed one, a permissions error and a path typo used to
54
+ * A missing file, a malformed one, a permissions error and a path typo would
55
55
  * be indistinguishable from each other *and* from a correctly configured build
56
- * with no symbols: a bare `catch` returned `{}` for all five. Nothing then
56
+ * with no symbols, were a bare `catch` to return `{}` for all of them. Nothing then
57
57
  * compares an emitted page against what its source asked for, so the first
58
58
  * observer of a broken map was a reader who clicked nothing, because every
59
59
  * `{@link}` on the published site had quietly become a code span.
@@ -112,7 +112,7 @@ function readSymbolMap(file, repoRoot) {
112
112
 
113
113
  // Reported because a map that loaded and a map that loaded empty are
114
114
  // otherwise indistinguishable without reading the emitted HTML — and an
115
- // empty one degrades every tag exactly as a missing one used to.
115
+ // empty one degrades every tag exactly as a missing one does.
116
116
  log.info(`resolved ${Object.keys(parsed).length} API symbols from ${file}`);
117
117
  return parsed;
118
118
  }
@@ -91,10 +91,12 @@ export function resolveFoundryVersion(stage: string, { env, stages, compatibilit
91
91
  /**
92
92
  * The image a run uses.
93
93
  *
94
- * `FOUNDRYVTT_CONTAINER_IMAGE` wins, then `packageBuild.container.image`, then
95
- * felddy's major tag for whichever version the package already implies — the
96
- * pinned build, or failing that the compatibility floor. Only a package that
97
- * claims neither floats.
94
+ * `FOUNDRYVTT_CONTAINER_IMAGE` wins, then `packageBuild.container.image`. A
95
+ * pinned exact build takes felddy's tag for that build: each image installs its
96
+ * own build by default and warns that a different `FOUNDRY_VERSION` "may not
97
+ * function properly". Otherwise felddy's major tag for whichever version the
98
+ * package implies — the pin, or failing that the compatibility floor. Only a
99
+ * package that claims neither floats.
98
100
  *
99
101
  * @param {object} [opts]
100
102
  * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
@@ -162,6 +164,37 @@ export function resolveDataRoot(stage: string, { env }?: {
162
164
  * @returns {[string, string][]} The pairs to pass through.
163
165
  */
164
166
  export function passthroughEnv(env?: NodeJS.ProcessEnv): [string, string][];
167
+ /**
168
+ * Whether the image has to sign in to foundryvtt.com to start.
169
+ *
170
+ * Given `FOUNDRY_USERNAME` and `FOUNDRY_PASSWORD`, the image signs in and asks
171
+ * for a download URL on every container it creates, even when the release is
172
+ * already in its cache. Each of those is a request foundryvtt.com rate-limits,
173
+ * and one refused with HTTP 429 stops the container before Foundry starts. The
174
+ * account serves two purposes only: downloading a release that is not cached,
175
+ * and fetching a licence key for an unlicensed data root. When neither applies
176
+ * the account is withheld, and the release installs from the cache with no
177
+ * network request.
178
+ *
179
+ * The image caches `foundryvtt-<version>.zip` in the mounted cache, or in
180
+ * `container_cache` beneath the data root when no cache is mounted.
181
+ *
182
+ * @param {object} opts
183
+ * @param {string|null} [opts.cacheDir] - The host cache mounted into the container.
184
+ * @param {string} opts.dataRoot - The host data root mounted at `/data`.
185
+ * @param {string|null} [opts.version] - The pinned build; with none, the
186
+ * archive's name is unknown.
187
+ * @param {string|null} [opts.licenseKey] - The licence key the image is given.
188
+ * @param {(file: string) => boolean} [opts.fileExists] - Replaceable for tests.
189
+ * @returns {boolean} Whether to pass the account through.
190
+ */
191
+ export function needsFoundryAccount({ cacheDir, dataRoot, version, licenseKey, fileExists, }: {
192
+ cacheDir?: string | null | undefined;
193
+ dataRoot: string;
194
+ version?: string | null | undefined;
195
+ licenseKey?: string | null | undefined;
196
+ fileExists?: ((file: string) => boolean) | undefined;
197
+ }): boolean;
165
198
  /**
166
199
  * The full `docker run` argument vector for a stage's container.
167
200
  *
@@ -179,9 +212,16 @@ export function passthroughEnv(env?: NodeJS.ProcessEnv): [string, string][];
179
212
  * @param {string|null} [opts.version] - Exact build to pin.
180
213
  * @param {string|null} [opts.world] - World to auto-launch; `""` forces none.
181
214
  * @param {string|null} [opts.licenseKey] - Dedicated licence key.
215
+ * @param {boolean} [opts.account] - Whether to pass `FOUNDRY_USERNAME` and
216
+ * `FOUNDRY_PASSWORD` through; see {@link needsFoundryAccount}.
217
+ * @param {string|null} [opts.user] - `--user` value to run the container as.
218
+ * `felddy/foundryvtt` runs Foundry as user 1000 and ignores `FOUNDRY_UID` and
219
+ * `FOUNDRY_GID`; under rootless Docker, container root is the host user, so
220
+ * `0:0` makes every file under the data root theirs. Blank or `null` leaves
221
+ * the image's own user.
182
222
  * @returns {string[]} Arguments after `docker`.
183
223
  */
184
- export function dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, version, world, licenseKey, }: {
224
+ export function dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, version, world, licenseKey, account, user, }: {
185
225
  name: string;
186
226
  image: string;
187
227
  port: number;
@@ -191,6 +231,8 @@ export function dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, vers
191
231
  version?: string | null | undefined;
192
232
  world?: string | null | undefined;
193
233
  licenseKey?: string | null | undefined;
234
+ account?: boolean | undefined;
235
+ user?: string | null | undefined;
194
236
  }): string[];
195
237
  /**
196
238
  * Run the `docker` CLI, inheriting stdio.
@@ -200,6 +242,70 @@ export function dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, vers
200
242
  * @throws {Error} When `docker` is not on `PATH`.
201
243
  */
202
244
  export function runDocker(args: string[]): number;
245
+ /**
246
+ * The `docker run` argument vector for a short-lived container that runs a
247
+ * shell script against a stage's data root.
248
+ *
249
+ * It uses the stage's own image and mounts the data root at `/data`, so the
250
+ * script sees files exactly as the Foundry container does. Under rootless
251
+ * Docker, only the container's user namespace can unlink what the container's
252
+ * user wrote, and only it can hand files to that user.
253
+ *
254
+ * @param {object} opts
255
+ * @param {string} opts.image - Image reference.
256
+ * @param {string} opts.dataRoot - Host directory to mount at `/data`.
257
+ * @param {string} opts.script - A `sh` script; `$1`, `$2` … are `args`.
258
+ * @param {string[]} [opts.args] - Positional arguments for the script.
259
+ * @param {string|null} [opts.user] - `--user` value; `null` runs as the image's
260
+ * own user.
261
+ * @returns {string[]} Arguments after `docker`.
262
+ */
263
+ export function dataRootScriptArgs({ image, dataRoot, script, args, user }: {
264
+ image: string;
265
+ dataRoot: string;
266
+ script: string;
267
+ args?: string[] | undefined;
268
+ user?: string | null | undefined;
269
+ }): string[];
270
+ /**
271
+ * Run a shell script against a stage's data root in a throwaway container.
272
+ *
273
+ * @param {object} opts - As for {@link dataRootScriptArgs}.
274
+ * @returns {string} The script's standard output.
275
+ * @throws {Error} When `docker` is missing or the script exits non-zero.
276
+ */
277
+ export function runDataRootScript(opts: object): string;
278
+ /**
279
+ * A remover for trees under a stage's data root that the container's user
280
+ * owns, run as root in a throwaway container on that data root — the same
281
+ * mechanism that hands an end-to-end install to the container's user.
282
+ *
283
+ * @param {object} opts
284
+ * @param {ResolvedContainer} opts.container - The stage's container.
285
+ * @param {(opts: object) => string} [opts.runScript] - Throwaway-container runner.
286
+ * @returns {(tree: string, where: {dataRoot: string}) => void} Removes one tree.
287
+ */
288
+ export function containerTreeRemover({ container, runScript }: {
289
+ container: ResolvedContainer;
290
+ runScript?: ((opts: object) => string) | undefined;
291
+ }): (tree: string, where: {
292
+ dataRoot: string;
293
+ }) => void;
294
+ /**
295
+ * The command {@link containerTreeRemover} runs, written out for a person to
296
+ * paste when the deploy could not run it.
297
+ *
298
+ * @param {object} opts
299
+ * @param {string} opts.image - Image reference.
300
+ * @param {string} opts.dataRoot - Host directory mounted at `/data`.
301
+ * @param {string} opts.tree - The tree to remove, beneath the data root.
302
+ * @returns {string} A `docker run` command line.
303
+ */
304
+ export function containerRemovalCommand({ image, dataRoot, tree }: {
305
+ image: string;
306
+ dataRoot: string;
307
+ tree: string;
308
+ }): string;
203
309
  /**
204
310
  * Run the `docker` CLI and capture its output, tolerating failure.
205
311
  *
@@ -255,6 +361,7 @@ export function clearStaleLock(dataRoot: string, log?: (message: string) => void
255
361
  * @property {string|null} world
256
362
  * @property {string|null} licenseKey
257
363
  * @property {string|null} cacheDir
364
+ * @property {string|null} user - From `FOUNDRYVTT_CONTAINER_USER`.
258
365
  */
259
366
  /**
260
367
  * Resolve a stage's container settings.
@@ -367,4 +474,8 @@ export type ResolvedContainer = {
367
474
  world: string | null;
368
475
  licenseKey: string | null;
369
476
  cacheDir: string | null;
477
+ /**
478
+ * - From `FOUNDRYVTT_CONTAINER_USER`.
479
+ */
480
+ user: string | null;
370
481
  };
@@ -87,28 +87,6 @@ export namespace DEFAULT_PATHS {
87
87
  * @satisfies {readonly PackDocumentType[]}
88
88
  */
89
89
  export const PACK_DOCUMENT_TYPES: readonly ["Actor", "Adventure", "Item", "JournalEntry", "Macro", "Scene"];
90
- /**
91
- * Address-scheme keys a configuration may no longer declare.
92
- *
93
- * A retired key has exactly two possible fates, and only one of them is honest
94
- * — the same reasoning `engine/retired-fields.mjs` applies to a retired
95
- * frontmatter field. Left honoured, it keeps doing whatever it did, which is
96
- * why it was retired. Left *ignored*, it reads to its author as though it still
97
- * works: the configuration says one thing and the build does another, and
98
- * nothing says so. This module has no third option, because it has no warning
99
- * channel — every finding goes through `fail()`, which throws. So a retired
100
- * key is **refused**, at the line it was written on, with a message that says
101
- * the mechanism is gone rather than naming a value to correct.
102
- *
103
- * **`landing` is one such key.** It named which note addressed a whole section
104
- * rather than a page within one — a *landing page*, which therefore had no slug
105
- * of its own. There are no sections to address: a section is a Hugo content
106
- * directory the note format does not carry, a page's address names no
107
- * directory, and so no note lands anything.
108
- *
109
- * @type {Readonly<Record<string, string>>}
110
- */
111
- export const RETIRED_ADDRESS_KEYS: Readonly<Record<string, string>>;
112
90
  /**
113
91
  * A repository's address scheme, with the defaults an unconfigured one gets.
114
92
  *
@@ -119,9 +97,7 @@ export const RETIRED_ADDRESS_KEYS: Readonly<Record<string, string>>;
119
97
  * knowledge, held in `PACKAGE_BASE` (`engine/content-address.mjs`) and prefixed at
120
98
  * resolve time, so it is never recorded here.
121
99
  *
122
- * It is the whole scheme: `landing`, the key that named which note addressed a
123
- * whole section, is retired with the sections themselves — see
124
- * {@link RETIRED_ADDRESS_KEYS}.
100
+ * It is the whole scheme.
125
101
  */
126
102
  export const DEFAULT_ADDRESS_SCHEME: Readonly<{
127
103
  prefix: "";
@@ -136,6 +112,16 @@ export const DEFAULT_ADDRESS_SCHEME: Readonly<{
136
112
  * @type {readonly string[]}
137
113
  */
138
114
  export const RELATIONSHIP_KEYS: readonly string[];
115
+ /**
116
+ * The closed list a person's `role` in `authors` is drawn from.
117
+ *
118
+ * One role per entry: a person with two roles is listed twice. Printed in
119
+ * title case after the name on a book's title page; the Foundry manifest,
120
+ * whose author schema has no role, never receives it.
121
+ *
122
+ * @type {readonly string[]}
123
+ */
124
+ export const AUTHOR_ROLES: readonly string[];
139
125
  /**
140
126
  * How the loader hands {@link defineConfig} the system version it resolved.
141
127
  *
@@ -164,6 +150,44 @@ export const DERIVED_SYSTEM_VERSION: symbol;
164
150
  * @type {Readonly<Record<string, string>>}
165
151
  */
166
152
  export const DERIVED_HUGO_KEYS: Readonly<Record<string, string>>;
153
+ /**
154
+ * One declared book, resolved.
155
+ */
156
+ export type ResolvedPdfBook = {
157
+ /**
158
+ * Its name on the command line, `--book <id>`.
159
+ */
160
+ id: string;
161
+ title: string;
162
+ /**
163
+ * `""` when none is declared.
164
+ */
165
+ subtitle: string;
166
+ /**
167
+ * Absolute; `""` when none is declared.
168
+ * Whether the file exists is checked when the
169
+ * book is built, not here.
170
+ */
171
+ coverImage: string;
172
+ /**
173
+ * Absolute paths of the front matter.
174
+ */
175
+ front: readonly string[];
176
+ /**
177
+ * The file stem; the version is appended.
178
+ */
179
+ out: string;
180
+ /**
181
+ * The tree written inline, or
182
+ * `null` when `document` names a file.
183
+ */
184
+ contents: readonly object[] | null;
185
+ /**
186
+ * Absolute path of the tree's file, or `""`
187
+ * when the tree is written inline.
188
+ */
189
+ document: string;
190
+ };
167
191
  export type PackageKind = "systems" | "modules" | "documentation";
168
192
  export type PackDocumentType = "Actor" | "Adventure" | "Item" | "JournalEntry" | "Macro" | "Scene";
169
193
  /**
@@ -573,6 +597,26 @@ export type ItemRegistrySpec = {
573
597
  */
574
598
  builders: Record<string, ItemBuilderEntry>;
575
599
  };
600
+ /**
601
+ * One person in the top-level `authors` list.
602
+ */
603
+ export type AuthorSpec = {
604
+ /**
605
+ * As printed.
606
+ */
607
+ name: string;
608
+ email?: string | undefined;
609
+ url?: string | undefined;
610
+ /**
611
+ * A Discord handle, as Foundry's author schema
612
+ * carries one.
613
+ */
614
+ discord?: string | undefined;
615
+ /**
616
+ * One of {@link AUTHOR_ROLES}.
617
+ */
618
+ role?: string | undefined;
619
+ };
576
620
  /**
577
621
  * The configuration a consumer writes.
578
622
  */
@@ -606,14 +650,12 @@ export type ContentBuildConfigInput = {
606
650
  */
607
651
  homepage?: string | undefined;
608
652
  /**
609
- * `package.json`'s own `author`, in
610
- * either of npm's forms.
653
+ * The people who made the
654
+ * package. The Foundry manifest's
655
+ * `authors` and the site's byline
656
+ * are generated from it.
611
657
  */
612
- author?: string | {
613
- name: string;
614
- email?: string;
615
- url?: string;
616
- } | undefined;
658
+ authors?: AuthorSpec[] | undefined;
617
659
  /**
618
660
  * Whether the package is a system, a
619
661
  * module, or documentation — the kind
@@ -720,9 +762,15 @@ export type ContentBuildConfig = {
720
762
  */
721
763
  homepage: string | null;
722
764
  /**
723
- * `package.json`'s own `author`, normalised
724
- * from either of npm's forms; `null` when
725
- * the package declares none.
765
+ * The people who made
766
+ * the package, in the order declared; `[]`
767
+ * when none are.
768
+ */
769
+ authors: readonly Readonly<AuthorSpec>[];
770
+ /**
771
+ * Derived: the first of `authors`, in npm's
772
+ * `author` shape — the site's byline.
773
+ * `null` when no author is declared.
726
774
  */
727
775
  author: Readonly<{
728
776
  name: string;
@@ -85,9 +85,17 @@ export function buildConnection(stageUpper: string, remote: {
85
85
  *
86
86
  * @param {string} srcAbs - The staged tree.
87
87
  * @param {string} destDir - Where the package installs.
88
- * @returns {Promise<void>}
88
+ * @param {object} [opts]
89
+ * @param {(tree: string) => void|Promise<void>} [opts.removeTree] - Removes a
90
+ * previous tree the host user is refused permission to remove.
91
+ * @returns {Promise<{leftovers: LeftoverTree[]}>} The previous trees that
92
+ * could not be removed; the new one is installed either way.
89
93
  */
90
- export function deployLocal(srcAbs: string, destDir: string): Promise<void>;
94
+ export function deployLocal(srcAbs: string, destDir: string, { removeTree }?: {
95
+ removeTree?: ((tree: string) => void | Promise<void>) | undefined;
96
+ }): Promise<{
97
+ leftovers: LeftoverTree[];
98
+ }>;
91
99
  /**
92
100
  * Mirror the staged build into a remote directory over SFTP, with the same
93
101
  * staged swap {@link deployLocal} performs.
@@ -100,11 +108,14 @@ export function deployLocal(srcAbs: string, destDir: string): Promise<void>;
100
108
  * @param {string} remoteDir - Where the package installs on the host.
101
109
  * @param {object} [opts]
102
110
  * @param {(uploaded: string) => void} [opts.onUpload] - Per-file progress.
103
- * @returns {Promise<void>}
111
+ * @returns {Promise<{leftovers: LeftoverTree[]}>} The previous tree when it
112
+ * could not be removed; the new one is installed either way.
104
113
  */
105
114
  export function deployRemote(conn: object, srcAbs: string, remoteDir: string, { onUpload }?: {
106
115
  onUpload?: ((uploaded: string) => void) | undefined;
107
- }): Promise<void>;
116
+ }): Promise<{
117
+ leftovers: LeftoverTree[];
118
+ }>;
108
119
  /**
109
120
  * Deploy a staged package to one stage, choosing the transport from the
110
121
  * configured destination.
@@ -117,10 +128,13 @@ export function deployRemote(conn: object, srcAbs: string, remoteDir: string, {
117
128
  * @param {NodeJS.ProcessEnv} [opts.env] - The environment to read.
118
129
  * @param {string} [opts.prefix] - Fallback variable prefix for SFTP overrides.
119
130
  * @param {(message: string) => void} [opts.log] - Progress reporting.
120
- * @returns {Promise<{stage: string, destination: string, remote: boolean}>}
131
+ * @param {RemoveTree} [opts.removeTree] - Removes a previous local tree the
132
+ * host user is refused permission to remove.
133
+ * @returns {Promise<{stage: string, destination: string, remote: boolean, leftovers: LeftoverTree[]}>}
134
+ * `leftovers` names each previous tree the deploy installed past but could not remove.
121
135
  * @throws {Error} On an unknown stage, or one with no destination configured.
122
136
  */
123
- export function deployStage({ stage, source, packageKind, packageId, env, prefix, log, }: {
137
+ export function deployStage({ stage, source, packageKind, packageId, env, prefix, log, removeTree, }: {
124
138
  stage: string;
125
139
  source: string;
126
140
  packageKind: "systems" | "modules";
@@ -128,10 +142,12 @@ export function deployStage({ stage, source, packageKind, packageId, env, prefix
128
142
  env?: NodeJS.ProcessEnv | undefined;
129
143
  prefix?: string | undefined;
130
144
  log?: ((message: string) => void) | undefined;
145
+ removeTree?: RemoveTree | undefined;
131
146
  }): Promise<{
132
147
  stage: string;
133
148
  destination: string;
134
149
  remote: boolean;
150
+ leftovers: LeftoverTree[];
135
151
  }>;
136
152
  /**
137
153
  * The environment variable naming each stage's Foundry data root.
@@ -146,3 +162,23 @@ export const STAGE_ENV_MAP: Readonly<{
146
162
  prod: "FOUNDRYVTT_PROD_DATA";
147
163
  test: "FOUNDRYVTT_TEST_DATA";
148
164
  }>;
165
+ /**
166
+ * A previous package tree a deploy could not remove after the swap.
167
+ */
168
+ export type LeftoverTree = {
169
+ /**
170
+ * - The tree, still under its `….old-<pid>` name.
171
+ */
172
+ path: string;
173
+ /**
174
+ * - Why the last attempt to remove it failed.
175
+ */
176
+ reason: string;
177
+ };
178
+ /**
179
+ * Removes a tree the host user cannot, typically by acting as the user that
180
+ * owns its files. Receives the tree and the stage's data root.
181
+ */
182
+ export type RemoveTree = (tree: string, where: {
183
+ dataRoot: string;
184
+ }) => void | Promise<void>;
package/types/e2e.d.mts CHANGED
@@ -146,25 +146,57 @@ export function parseFastArgs(argv: string[], build: Record<string, {
146
146
  *
147
147
  * Writes `world.json` and compiles each LevelDB collection from JSON, so the
148
148
  * result is a genuine world Foundry launches without migration or setup. The
149
- * world directory is wiped and rewritten each time, which is what makes a run
150
- * repeatable.
149
+ * world directory is replaced each time, which is what makes a run repeatable.
150
+ *
151
+ * The world is built in a staging directory beside the worlds, then moved into
152
+ * place and handed to the Foundry container's user by a throwaway container
153
+ * mounting the same data root. Replacing the old world happens there too. Both
154
+ * halves matter: a database the host user wrote cannot be locked by the
155
+ * container's user, and a directory the container's user wrote (such as the
156
+ * `lost/` folder Foundry's repair makes) cannot be unlinked by the host user.
151
157
  *
152
158
  * @param {object} opts
153
159
  * @param {object} opts.config - The resolved package-build configuration.
154
160
  * @param {object} opts.packageJson - The repository's `package.json`.
155
161
  * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
156
162
  * @param {(message: string) => void} [opts.log] - Progress reporting.
163
+ * @param {(opts: object) => string} [opts.runScript] - Runs a shell script in a
164
+ * throwaway container on the data root; replaceable for tests.
157
165
  * @returns {Promise<{worldDir: string, world: E2EWorld}>} Where it landed.
158
166
  */
159
- export function seedTestWorld({ config, packageJson, env, log }: {
167
+ export function seedTestWorld({ config, packageJson, env, log, runScript, }: {
160
168
  config: object;
161
169
  packageJson: object;
162
170
  env?: NodeJS.ProcessEnv | undefined;
163
171
  log?: ((message: string) => void) | undefined;
172
+ runScript?: ((opts: object) => string) | undefined;
164
173
  }): Promise<{
165
174
  worldDir: string;
166
175
  world: E2EWorld;
167
176
  }>;
177
+ /**
178
+ * Deploy the staged package to the end-to-end stage so the container's user
179
+ * can run it.
180
+ *
181
+ * The package's databases (compendium packs) are written by the host user, and
182
+ * Foundry cannot lock them unless it owns them; a pack Foundry has repaired
183
+ * holds directories the host user cannot unlink. So the previous install is
184
+ * removed, and the new one handed over, by a throwaway container on the same
185
+ * data root. A remote destination is deployed as {@link deployStage} does.
186
+ *
187
+ * @param {object} opts
188
+ * @param {object} opts.config - The resolved package-build configuration.
189
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
190
+ * @param {(message: string) => void} [opts.log] - Progress reporting.
191
+ * @param {(opts: object) => string} [opts.runScript] - Throwaway-container runner.
192
+ * @returns {Promise<void>}
193
+ */
194
+ export function deployForContainer({ config, env, log, runScript, }: {
195
+ config: object;
196
+ env?: NodeJS.ProcessEnv | undefined;
197
+ log?: ((message: string) => void) | undefined;
198
+ runScript?: ((opts: object) => string) | undefined;
199
+ }): Promise<void>;
168
200
  /**
169
201
  * Poll until the world is active, or fail with a diagnosis.
170
202
  *
@@ -40,26 +40,13 @@ export function isAddressSegment(value: unknown): boolean;
40
40
  * The shape every address segment must match: **lowercase** ASCII letters and
41
41
  * digits only.
42
42
  *
43
- * Case *was* deliberately unconstrained, on the reasoning that case has no
44
- * bearing on the separator — which is true, and beside the point.
45
- *
46
43
  * **Two names that differ only in case are two names nobody can tell apart.** A
47
44
  * shortcode is how a person names a thing when writing a reference —
48
45
  * `model: weapongear-dgr`, `[[skill-melee|…]]` — and `Dgr` beside `dgr` is a
49
- * distinction you cannot say out loud and can only see by looking twice.
50
- *
51
- * The toolchain had already half-decided it: {@link canonicalKey} lowercases the
52
- * address it builds, so a note declaring `Clb` published
53
- * `sohl-sohl-weapongear-clb` and its `_id` derived from that. The authored name
54
- * and its address disagreed, and everything downstream keys on the address —
55
- * which left two notes differing only in case sharing one address, one `_id` and
56
- * one URL, with nothing to report it. It also forces two exceptions elsewhere:
57
- * the shortcode has to be exempted from the lowercase rule pinned on every
58
- * other segment, and its case has to be folded in the item catalogue because
59
- * an address is lowercased when read.
60
- *
61
- * One case, one spelling, no exceptions. Every tree already complies but two,
62
- * and nothing in any of them collides when folded.
46
+ * distinction you cannot say out loud and can only see by looking twice. So the
47
+ * shortcode is held to the same rule as every other segment: a note declaring
48
+ * `shortcode: Dgr` is refused, and so is a declared Address spelling one with a
49
+ * capital.
63
50
  *
64
51
  * @type {RegExp}
65
52
  */
@@ -21,11 +21,16 @@
21
21
  * - `no-content-index` — the package named publishes no content index, so there
22
22
  * is nothing to resolve the rest of the target against.
23
23
  * - `not-lowercase` — a package, system or type segment carries a capital.
24
+ * - `shortcode-not-lowercase` — the Address spells its shortcode with a
25
+ * capital and is otherwise well formed. `shortcode` carries the segment as
26
+ * written, and `lowercase` the reading of the lowercase spelling: complete
27
+ * for a declared Address, and as partial as the written value otherwise.
24
28
  * - `invalid-segment` — a declared Address contains a malformed segment.
25
29
  * - `no-package` — a declared Address cannot supply its package.
26
30
  * - `invalid-system` — a declared Address cannot supply a registered system.
27
31
  *
28
- * @typedef {{reason: string, package?: string}} AddressProblem
32
+ * @typedef {{reason: string, package?: string, shortcode?: string,
33
+ * lowercase?: AddressTuple}} AddressProblem
29
34
  */
30
35
  /**
31
36
  * The configured types whose prose compiles into a separate JournalEntry.
@@ -94,17 +99,15 @@ export function resolveItemDocType(qualifier: string, types: Set<string>): strin
94
99
  * label already removed.
95
100
  * @param {AddressDefaults} [defaults] - The position's defaults and the tree's
96
101
  * vocabularies.
97
- * @param {{declared?: boolean, legacyShortcodeCase?: boolean}} [options] - A declared Address validates every
98
- * segment and requires complete defaults. Package membership is a separate
99
- * resolution question in this mode; prose recognition keeps its vocabulary checks.
100
- * `legacyShortcodeCase` accepts case-insensitive model Shortcodes while the
101
- * completed tuple contains their canonical lowercase identity.
102
+ * @param {{declared?: boolean}} [options] - A declared Address is validated as
103
+ * written: every segment, the shortcode included, is lowercase, and the
104
+ * defaults must be complete. Package membership is a separate resolution
105
+ * question in this mode; prose recognition keeps its vocabulary checks.
102
106
  * @returns {AddressTuple|AddressProblem} The complete Address, or why there is
103
107
  * none.
104
108
  */
105
- export function parseAddress(written: unknown, defaults?: AddressDefaults, { declared, legacyShortcodeCase }?: {
109
+ export function parseAddress(written: unknown, defaults?: AddressDefaults, { declared }?: {
106
110
  declared?: boolean;
107
- legacyShortcodeCase?: boolean;
108
111
  }): AddressTuple | AddressProblem;
109
112
  /**
110
113
  * Split a written value at its anchor.
@@ -343,6 +346,10 @@ export type AddressTuple = {
343
346
  * - `no-content-index` — the package named publishes no content index, so there
344
347
  * is nothing to resolve the rest of the target against.
345
348
  * - `not-lowercase` — a package, system or type segment carries a capital.
349
+ * - `shortcode-not-lowercase` — the Address spells its shortcode with a
350
+ * capital and is otherwise well formed. `shortcode` carries the segment as
351
+ * written, and `lowercase` the reading of the lowercase spelling: complete
352
+ * for a declared Address, and as partial as the written value otherwise.
346
353
  * - `invalid-segment` — a declared Address contains a malformed segment.
347
354
  * - `no-package` — a declared Address cannot supply its package.
348
355
  * - `invalid-system` — a declared Address cannot supply a registered system.
@@ -350,6 +357,8 @@ export type AddressTuple = {
350
357
  export type AddressProblem = {
351
358
  reason: string;
352
359
  package?: string;
360
+ shortcode?: string;
361
+ lowercase?: AddressTuple;
353
362
  };
354
363
  /**
355
364
  * The vocabularies a written Address is read against.
@@ -106,7 +106,8 @@ export function assetImageInfoByPathname(records?: readonly object[]): Map<strin
106
106
  * slot's. Omitted, any asset type is taken, which is an embed's rule: it
107
107
  * draws a file, and every asset type is a file.
108
108
  * @returns {{record: {package: string, asset: {path: string}}, pathname: string}
109
- * |{record: null, reason: string, type?: string}} The asset and the pathname
109
+ * |{record: null, reason: string, type?: string, shortcode?: string,
110
+ * declared?: string}} The asset and the pathname
110
111
  * it is at, or a reason from
111
112
  * {@link module:engine/wikilink-syntax.LINK_FINDING_REASONS}, or `not-accepted`
112
113
  * when `accepts` is given and the parsed type falls outside it.
@@ -123,6 +124,8 @@ export function readAssetAddress(index: object, value: unknown, defaultType: str
123
124
  record: null;
124
125
  reason: string;
125
126
  type?: string;
127
+ shortcode?: string;
128
+ declared?: string;
126
129
  };
127
130
  /**
128
131
  * The index record an art value resolves to, or `null`.
@@ -184,9 +187,9 @@ export function checkArtSlotSizes(records?: readonly object[], { config, assetsB
184
187
  * The pathname an art value names, in the form an authored one takes.
185
188
  *
186
189
  * Handing the result to {@link module:engine/helpers.resolveImg} is what puts an
187
- * art address and a body image through one ownership rule. The two empties
188
- * survive it unchanged: `null` and an absent key mean *no art named, apply the
189
- * default*, and `""` means *ship blank on purpose*.
190
+ * art address and a body image through one ownership rule. `null` and an
191
+ * absent key mean *no art named, apply the default*; an empty string names
192
+ * nothing and is unresolved like any other address nothing answers.
190
193
  *
191
194
  * @param {object} index - From {@link module:engine/wikilinks.buildWikilinkIndex}.
192
195
  * @param {unknown} value - The value as authored.
@@ -34,6 +34,12 @@ export type ArtSlot = {
34
34
  * - Whether the slot reaches a compiled document.
35
35
  */
36
36
  document: boolean;
37
+ /**
38
+ * - The literal value that declines the slot's
39
+ * art on purpose. It is never read as an Address: it resolves to no image and
40
+ * is emitted as a blank, which a theme reads as "draw none".
41
+ */
42
+ declines?: string | undefined;
37
43
  /**
38
44
  * - One line, for the author-facing reference.
39
45
  */