@heroiclands/package-build 17.1.0 → 18.0.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 (153) hide show
  1. package/CHANGELOG.md +1773 -0
  2. package/CONTENT.md +288 -72
  3. package/README.md +1 -1
  4. package/bin/content-build.mjs +218 -137
  5. package/bin/package-build.mjs +43 -0
  6. package/content-config.mjs +62 -67
  7. package/docs/content-format.md +768 -58
  8. package/engine/actor-compiler.mjs +586 -0
  9. package/engine/address-charset.mjs +11 -8
  10. package/engine/address-diff.mjs +266 -22
  11. package/engine/anchored-sections.mjs +83 -0
  12. package/engine/anchors.mjs +83 -0
  13. package/engine/base-compiler.mjs +148 -18
  14. package/engine/bundle-notes.mjs +276 -0
  15. package/engine/bundles.mjs +307 -0
  16. package/engine/code-fences.mjs +103 -0
  17. package/engine/compile-corpus.mjs +89 -0
  18. package/engine/content-address.mjs +267 -5
  19. package/engine/content-format-check.mjs +54 -11
  20. package/engine/content-format.mjs +37 -5
  21. package/engine/content-index.mjs +394 -111
  22. package/engine/content-links.mjs +185 -77
  23. package/engine/content-lint.mjs +232 -14
  24. package/engine/content-package.mjs +2 -1
  25. package/engine/content-tables.mjs +99 -3
  26. package/engine/document-subtypes.mjs +49 -6
  27. package/engine/field-reference.mjs +1 -1
  28. package/engine/field-spec.mjs +56 -6
  29. package/engine/folder-notes.mjs +470 -0
  30. package/engine/foreign-catalog.mjs +195 -4
  31. package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
  32. package/engine/frontmatter-lint.mjs +245 -19
  33. package/engine/frontmatter.mjs +88 -8
  34. package/engine/generate.mjs +304 -13
  35. package/engine/helpers.mjs +293 -64
  36. package/engine/ids.mjs +137 -2
  37. package/engine/index-records.mjs +126 -0
  38. package/engine/index.mjs +23 -5
  39. package/engine/item-compiler.mjs +349 -0
  40. package/engine/item-docs.mjs +7 -1
  41. package/engine/item-registry.mjs +6 -0
  42. package/engine/journals.mjs +93 -27
  43. package/engine/macros.mjs +4 -2
  44. package/engine/metadata-index.mjs +495 -0
  45. package/engine/note-claims.mjs +146 -25
  46. package/engine/note-ids.mjs +112 -0
  47. package/engine/note-renames.mjs +134 -0
  48. package/engine/note-schemas.mjs +25 -0
  49. package/engine/note-vocabulary.mjs +110 -27
  50. package/engine/pack-config.mjs +4 -0
  51. package/engine/pack-router.mjs +23 -0
  52. package/engine/retired-fields.mjs +137 -3
  53. package/engine/scenes.mjs +66 -34
  54. package/engine/schema-check.mjs +58 -17
  55. package/engine/site-build.mjs +71 -12
  56. package/engine/site-index.mjs +20 -7
  57. package/engine/sql-tables.mjs +485 -0
  58. package/engine/subtype-registry.mjs +102 -0
  59. package/engine/system-block.mjs +96 -12
  60. package/engine/systems.mjs +201 -0
  61. package/engine/web-wikilinks.mjs +50 -20
  62. package/engine/wikilink-syntax.mjs +17 -8
  63. package/engine/wikilinks.mjs +124 -51
  64. package/engine/yaml-lint.mjs +282 -0
  65. package/hm3/actors.mjs +352 -0
  66. package/hm3/default-item-art.mjs +75 -0
  67. package/hm3/document-subtypes.mjs +134 -0
  68. package/hm3/index.mjs +56 -0
  69. package/hm3/item-builders.mjs +84 -0
  70. package/hm3/item-fields.mjs +180 -0
  71. package/hm3/items.mjs +96 -0
  72. package/hm3/template-priority.mjs +85 -0
  73. package/labels.mjs +191 -0
  74. package/manifest.mjs +43 -2
  75. package/package.json +19 -4
  76. package/release.mjs +62 -7
  77. package/sohl/actors.mjs +33 -487
  78. package/sohl/being-info.mjs +16 -7
  79. package/sohl/default-item-art.mjs +14 -3
  80. package/sohl/document-subtypes.mjs +16 -10
  81. package/sohl/item-builders.mjs +14 -5
  82. package/sohl/item-fields.mjs +68 -7
  83. package/sohl/items.mjs +44 -258
  84. package/sohl/note-schemas.mjs +2 -2
  85. package/types/content-config.d.mts +21 -42
  86. package/types/engine/actor-compiler.d.mts +204 -0
  87. package/types/engine/address-charset.d.mts +11 -8
  88. package/types/engine/address-diff.d.mts +53 -5
  89. package/types/engine/anchored-sections.d.mts +21 -0
  90. package/types/engine/anchors.d.mts +20 -0
  91. package/types/engine/base-compiler.d.mts +17 -17
  92. package/types/engine/bundle-notes.d.mts +173 -0
  93. package/types/engine/bundles.d.mts +60 -0
  94. package/types/engine/code-fences.d.mts +43 -0
  95. package/types/engine/compile-corpus.d.mts +32 -0
  96. package/types/engine/content-address.d.mts +205 -5
  97. package/types/engine/content-format-check.d.mts +6 -2
  98. package/types/engine/content-format.d.mts +57 -1
  99. package/types/engine/content-index.d.mts +64 -80
  100. package/types/engine/content-links.d.mts +52 -7
  101. package/types/engine/content-lint.d.mts +10 -1
  102. package/types/engine/content-package.d.mts +2 -1
  103. package/types/engine/content-tables.d.mts +21 -39
  104. package/types/engine/document-subtypes.d.mts +37 -3
  105. package/types/engine/field-spec.d.mts +76 -5
  106. package/types/engine/folder-notes.d.mts +159 -0
  107. package/types/engine/foreign-catalog.d.mts +53 -0
  108. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
  109. package/types/engine/frontmatter-lint.d.mts +10 -2
  110. package/types/engine/frontmatter.d.mts +64 -0
  111. package/types/engine/generate.d.mts +38 -0
  112. package/types/engine/helpers.d.mts +94 -30
  113. package/types/engine/ids.d.mts +96 -0
  114. package/types/engine/index-records.d.mts +68 -0
  115. package/types/engine/index.d.mts +9 -3
  116. package/types/engine/item-compiler.d.mts +131 -0
  117. package/types/engine/journals.d.mts +47 -9
  118. package/types/engine/metadata-index.d.mts +226 -0
  119. package/types/engine/note-claims.d.mts +57 -11
  120. package/types/engine/note-ids.d.mts +38 -0
  121. package/types/engine/note-renames.d.mts +102 -0
  122. package/types/engine/note-vocabulary.d.mts +44 -8
  123. package/types/engine/retired-fields.d.mts +75 -0
  124. package/types/engine/scenes.d.mts +3 -2
  125. package/types/engine/schema-check.d.mts +25 -4
  126. package/types/engine/site-build.d.mts +4 -4
  127. package/types/engine/site-index.d.mts +1 -1
  128. package/types/engine/sql-tables.d.mts +185 -0
  129. package/types/engine/subtype-registry.d.mts +49 -0
  130. package/types/engine/system-block.d.mts +40 -1
  131. package/types/engine/systems.d.mts +106 -0
  132. package/types/engine/web-wikilinks.d.mts +4 -2
  133. package/types/engine/wikilink-syntax.d.mts +10 -3
  134. package/types/engine/wikilinks.d.mts +41 -13
  135. package/types/engine/yaml-lint.d.mts +107 -0
  136. package/types/hm3/actors.d.mts +48 -0
  137. package/types/hm3/default-item-art.d.mts +42 -0
  138. package/types/hm3/document-subtypes.d.mts +24 -0
  139. package/types/hm3/index.d.mts +7 -0
  140. package/types/hm3/item-builders.d.mts +11 -0
  141. package/types/hm3/item-fields.d.mts +12 -0
  142. package/types/hm3/items.d.mts +23 -0
  143. package/types/hm3/template-priority.d.mts +21 -0
  144. package/types/manifest.d.mts +22 -1
  145. package/types/release.d.mts +9 -4
  146. package/types/sohl/actors.d.mts +7 -74
  147. package/types/sohl/being-info.d.mts +8 -27
  148. package/types/sohl/default-item-art.d.mts +5 -3
  149. package/types/sohl/items.d.mts +17 -32
  150. package/engine/foreign-manifests.mjs +0 -126
  151. package/engine/kb-manifest.mjs +0 -490
  152. package/types/engine/foreign-manifests.d.mts +0 -43
  153. package/types/engine/kb-manifest.d.mts +0 -241
package/manifest.mjs CHANGED
@@ -53,6 +53,7 @@ import fsSync from "node:fs";
53
53
  import path from "node:path";
54
54
 
55
55
  import { emitDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
56
+ import { metadataFileName } from "./engine/metadata-index.mjs";
56
57
 
57
58
  /**
58
59
  * The two package kinds Foundry defines, as the artifact name each one's
@@ -97,7 +98,7 @@ export function normalizeRepoUrl(repository) {
97
98
  }
98
99
 
99
100
  /**
100
- * The four addresses a Foundry manifest advertises.
101
+ * The addresses a Foundry manifest advertises.
101
102
  *
102
103
  * `manifest` deliberately points at **`releases/latest`** rather than at this
103
104
  * version: it is the URL an *installed* package re-fetches to discover that a
@@ -120,6 +121,26 @@ export function releaseUrls({ repoUrl, version, artifact }) {
120
121
  };
121
122
  }
122
123
 
124
+ /**
125
+ * Where this release publishes its content index (#239).
126
+ *
127
+ * **Pinned to this version, like `download` and unlike `manifest`.** A
128
+ * consumer reaches this URL by reading the dependency's manifest, so the
129
+ * manifest it read and the index it then fetches describe the same release —
130
+ * which is the whole point of publishing them together. A `releases/latest`
131
+ * index would silently pair a pinned manifest with a moving index, and the
132
+ * mismatch would surface as a cross-package link that resolved yesterday.
133
+ *
134
+ * @param {object} opts
135
+ * @param {string} opts.repoUrl - Normalised repository URL.
136
+ * @param {string} opts.version - The version being built.
137
+ * @param {string} opts.contentPackage - The content package name.
138
+ * @returns {string} The version-pinned asset URL.
139
+ */
140
+ export function metadataUrl({ repoUrl, version, contentPackage }) {
141
+ return `${repoUrl}/releases/download/v${version}/${metadataFileName(contentPackage)}`;
142
+ }
143
+
123
144
  /**
124
145
  * The order the manifest's keys are written in.
125
146
  *
@@ -455,8 +476,28 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
455
476
 
456
477
  const merged = { ...declared, ...derived };
457
478
 
479
+ // The index every consumer resolves this package's addresses through
480
+ // (#239). Written unconditionally, because a package that publishes no
481
+ // index is one nothing can link into — and the failure of an absent one is
482
+ // a dead link in somebody else's build, which is exactly the kind of
483
+ // silence this replaced the vendored manifest to end.
484
+ //
485
+ // A flag rather than a top-level key because Foundry's manifest schema is
486
+ // closed and `flags` is its declared extension point; an unknown key at the
487
+ // top level is dropped by some readers and rejected by others.
488
+ if (config.contentPackage) {
489
+ merged.flags = {
490
+ ...(declared.flags ?? {}),
491
+ metadataUrl: metadataUrl({
492
+ repoUrl,
493
+ version: packageJson.version,
494
+ contentPackage: config.contentPackage,
495
+ }),
496
+ };
497
+ }
498
+
458
499
  if (flags && Object.keys(flags).length) {
459
- merged.flags = { ...(declared.flags ?? {}) };
500
+ merged.flags = { ...(merged.flags ?? declared.flags ?? {}) };
460
501
  for (const [namespace, values] of Object.entries(flags)) {
461
502
  merged.flags[namespace] = {
462
503
  ...(declared.flags?.[namespace] ?? {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "17.1.0",
3
+ "version": "18.0.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -27,6 +27,14 @@
27
27
  "types": "./types/sohl/*.d.mts",
28
28
  "import": "./sohl/*.mjs"
29
29
  },
30
+ "./hm3": {
31
+ "types": "./types/hm3/index.d.mts",
32
+ "import": "./hm3/index.mjs"
33
+ },
34
+ "./hm3/*": {
35
+ "types": "./types/hm3/*.d.mts",
36
+ "import": "./hm3/*.mjs"
37
+ },
30
38
  "./content-config": {
31
39
  "types": "./types/content-config.d.mts",
32
40
  "import": "./content-config.mjs"
@@ -105,8 +113,10 @@
105
113
  "deploy.mjs",
106
114
  "e2e.mjs",
107
115
  "engine",
116
+ "hm3",
108
117
  "index.mjs",
109
118
  "lang.mjs",
119
+ "labels.mjs",
110
120
  "manifest.mjs",
111
121
  "markdownlint-config.mjs",
112
122
  "prettier-config.mjs",
@@ -117,11 +127,14 @@
117
127
  "types"
118
128
  ],
119
129
  "dependencies": {
130
+ "@duckdb/node-api": "^1.5.5-r.4",
120
131
  "@foundryvtt/foundryvtt-cli": "^3.0.4",
121
132
  "acorn": "^8.18.0",
122
133
  "archiver": "^8.0.0",
123
134
  "classic-level": "^3.0.0",
124
135
  "dotenv": "^17.2.3",
136
+ "eslint": "^10.10.0",
137
+ "eslint-plugin-yml": "^3.8.1",
125
138
  "fflate": "^0.8.3",
126
139
  "glob": "^13.0.6",
127
140
  "gray-matter": "^4.0.3",
@@ -149,12 +162,14 @@
149
162
  "build:types": "tsc -p tsconfig.dts.json",
150
163
  "format": "prettier --write .",
151
164
  "format:check": "prettier --check .",
152
- "lint": "npm run format:check && npm run lint:markdown && npm run lint:content-format",
165
+ "lint": "npm run format:check && npm run lint:markdown && npm run lint:yaml && npm run lint:labels && npm run lint:content-format",
153
166
  "lint:markdown": "node bin/content-build.mjs markdown",
167
+ "lint:yaml": "node bin/package-build.mjs yaml",
168
+ "lint:labels": "node bin/package-build.mjs labels check",
154
169
  "lint:markdown:fix": "node bin/content-build.mjs markdown --fix",
155
170
  "lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields",
156
- "lint:content-format:schema": "node bin/content-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json",
157
- "lint:content-format:fields": "node bin/content-build.mjs content-format fields --fields sohl",
171
+ "lint:content-format:schema": "node bin/content-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json --schema hm3=tests/fixtures/content-format/schema-hm3.json",
172
+ "lint:content-format:fields": "node bin/content-build.mjs content-format fields --fields sohl && node bin/content-build.mjs content-format fields --fields hm3",
158
173
  "changeset": "changeset",
159
174
  "changeset:check": "changeset status --since=origin/main",
160
175
  "changeset:version": "changeset version && npm install --package-lock-only",
package/release.mjs CHANGED
@@ -16,10 +16,12 @@
16
16
  * carries.
17
17
  *
18
18
  * Foundry installs a package by fetching the `download` URL its manifest
19
- * advertises, so a release publishes exactly two assets: `<artifact>.zip`, the
20
- * whole staged tree, and `<artifact>.json` beside it, which is what an already
21
- * installed package re-fetches to notice a new version. Both names are fixed by
22
- * what the manifest says, not chosen here see `manifest.mjs`.
19
+ * advertises, so a release publishes `<artifact>.zip`, the whole staged tree,
20
+ * and `<artifact>.json` beside it, which is what an already installed package
21
+ * re-fetches to notice a new version. A package that ships content publishes a
22
+ * third: the content index other packages resolve its addresses through (#239),
23
+ * named by the `flags.metadataUrl` the manifest advertises. Every name is fixed
24
+ * by what the manifest says, not chosen here — see `manifest.mjs`.
23
25
  *
24
26
  * Kept apart from `stage.mjs` because this is the only part of assembling a
25
27
  * package that needs a dependency. A repository that never cuts a release from
@@ -53,9 +55,12 @@ import { ZipArchive } from "archiver";
53
55
  * @param {string} [opts.outDir] - Where the release assets are written.
54
56
  * @param {"system"|"module"} [opts.artifact] - Which artifact is shipped.
55
57
  * Determines both asset names.
56
- * @returns {Promise<{zip: string, manifest: string, bytes: number,
57
- * version: string}>} The two paths written, the archive's size, and the
58
- * version the manifest declares.
58
+ * @param {string} [opts.metadataDir] - Where the build writes its content
59
+ * index, consulted when the advertised file was not staged.
60
+ * @returns {Promise<{zip: string, manifest: string, metadata?: string,
61
+ * bytes: number, version: string}>} The paths written, the archive's size,
62
+ * and the version the manifest declares. `metadata` is absent when the
63
+ * manifest advertises no content index.
59
64
  * @throws {Error} When the stage has no manifest — there is nothing to release,
60
65
  * and an archive without one installs as nothing.
61
66
  */
@@ -63,6 +68,7 @@ export async function packRelease({
63
68
  stageDir = "build/stage",
64
69
  outDir = "build/dist",
65
70
  artifact = "system",
71
+ metadataDir = "build/content-index",
66
72
  } = {}) {
67
73
  const stage = path.resolve(stageDir);
68
74
  const out = path.resolve(outDir);
@@ -104,10 +110,59 @@ export async function packRelease({
104
110
 
105
111
  await fsp.copyFile(stagedManifest, path.join(out, manifestName));
106
112
 
113
+ const metadata = await publishMetadataIndex({ manifest, stage, out, metadataDir });
114
+
107
115
  return {
108
116
  zip: zipPath,
109
117
  manifest: path.join(out, manifestName),
118
+ ...(metadata ? { metadata } : {}),
110
119
  bytes: archive.pointer(),
111
120
  version: manifest.version,
112
121
  };
113
122
  }
123
+
124
+ /**
125
+ * Place the content index the manifest advertises beside the archive (#239).
126
+ *
127
+ * **The asset's name comes from the manifest, not from here.** `flags.metadataUrl`
128
+ * is the URL every consumer fetches, so its basename is by definition the name
129
+ * the file has to be published under — deriving it a second time would let the
130
+ * two disagree, and a release whose asset is named differently from its
131
+ * advertised URL fails at the consumer, not here.
132
+ *
133
+ * A manifest that advertises no index publishes none: a package with no content
134
+ * tree has nothing to index, and is perfectly releasable. But a manifest that
135
+ * *does* advertise one and cannot produce it is a build error. The alternative
136
+ * is a release that promises an index it does not carry, whose symptom is a
137
+ * dead cross-package link in somebody else's build weeks later — the silent
138
+ * failure the vendored manifest was replaced to end.
139
+ *
140
+ * @param {object} opts
141
+ * @param {object} opts.manifest - The parsed staged manifest.
142
+ * @param {string} opts.stage - The staged tree.
143
+ * @param {string} opts.out - Where release assets are written.
144
+ * @param {string} opts.metadataDir - Where the build writes its index, tried
145
+ * when the file was not staged.
146
+ * @returns {Promise<string|undefined>} The published path, or nothing when the
147
+ * manifest advertises no index.
148
+ * @throws {Error} When one is advertised and no file backs it.
149
+ */
150
+ async function publishMetadataIndex({ manifest, stage, out, metadataDir }) {
151
+ const url = manifest.flags?.metadataUrl;
152
+ if (!url) return undefined;
153
+
154
+ const name = path.basename(new URL(url, "https://example.invalid").pathname);
155
+ const candidates = [path.join(stage, name), path.resolve(metadataDir, name)];
156
+ const found = candidates.find((c) => fs.existsSync(c));
157
+ if (!found) {
158
+ throw new Error(
159
+ `the manifest advertises ${name} as \`flags.metadataUrl\` but no such ` +
160
+ `file exists — looked in ${candidates.join(" and ")}. Build the ` +
161
+ `content index before packing the release.`,
162
+ );
163
+ }
164
+
165
+ const dest = path.join(out, name);
166
+ await fsp.copyFile(found, dest);
167
+ return dest;
168
+ }