@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.
- package/CHANGELOG.md +1773 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +394 -111
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/labels.mjs +191 -0
- package/manifest.mjs +43 -2
- package/package.json +19 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +64 -80
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
package/engine/kb-manifest.mjs
DELETED
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
-
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
-
*
|
|
5
|
-
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
-
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
-
*
|
|
8
|
-
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
-
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
-
*
|
|
11
|
-
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The cross-package link manifest (#1446).
|
|
16
|
-
*
|
|
17
|
-
* Each publishing package emits one file naming every note it publishes, keyed
|
|
18
|
-
* by the canonical `type/shortcode` address and valued with every address that
|
|
19
|
-
* note has: a `path` on the web, a `uuid` in Foundry.
|
|
20
|
-
* {@link loadForeignManifests} resolves each `path` into the `{ url, name }`
|
|
21
|
-
* the knowledgebase already uses as its own index value, so a foreign entry and
|
|
22
|
-
* a local one are interchangeable at the point of use.
|
|
23
|
-
*
|
|
24
|
-
* **Both addresses are optional, independently** (#1516). A note that publishes
|
|
25
|
-
* a page and compiles into no document has no `uuid`; a package that ships
|
|
26
|
-
* compendiums and publishes no site has no `path` on any entry. Neither is an
|
|
27
|
-
* error, and a consumer that cannot use the address it wanted must degrade —
|
|
28
|
-
* inventing the missing one asserts a target that does not exist, which is the
|
|
29
|
-
* silent dead link this whole format exists to prevent.
|
|
30
|
-
*
|
|
31
|
-
* The manifest exists to make one question decidable: when a link addresses
|
|
32
|
-
* `creature-grkrahk` and this build has never heard of it, is that a typo or a
|
|
33
|
-
* note belonging to another package? Before the manifest nothing in the syntax
|
|
34
|
-
* answered that, so the dead-link guard had to be left off for the hyphen form
|
|
35
|
-
* or correct content would fail the build (see `web-wikilinks.mjs`). With every
|
|
36
|
-
* package's manifest vendored, an address that resolves in none of them is a
|
|
37
|
-
* typo, and the guard can be restored.
|
|
38
|
-
*
|
|
39
|
-
* `kethira` is deliberately absent, on a **licensing** ground rather than a
|
|
40
|
-
* technical one: nothing may depend on it, because the module has to stay
|
|
41
|
-
* withdrawable (see that repository's `CLAUDE.md`), and a manifest edge
|
|
42
|
-
* pointing into it is exactly such a dependency. That it ships only packs is
|
|
43
|
-
* not the reason — since #1516 a pack-only package can publish a manifest.
|
|
44
|
-
*
|
|
45
|
-
* **An entry's address is relative to its own package's base** (#1465), never a
|
|
46
|
-
* site-absolute path. Where a package is *mounted* is the consumer's knowledge,
|
|
47
|
-
* held in {@link PACKAGE_BASE} and prefixed at resolve time — so moving a
|
|
48
|
-
* package to another path or origin is one string per consumer rather than a
|
|
49
|
-
* regenerated manifest, and an inbound link survives the move. A path recorded
|
|
50
|
-
* in the manifest would not: it resolves, emits an `href`, and 404s, which is
|
|
51
|
-
* the silent failure the manifest exists to end.
|
|
52
|
-
*/
|
|
53
|
-
|
|
54
|
-
import fs from "node:fs";
|
|
55
|
-
import path from "node:path";
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Packages that publish a manifest and therefore exchange addresses.
|
|
59
|
-
*
|
|
60
|
-
* The guard in {@link manifestsComplete} stays off until every one of these is
|
|
61
|
-
* accounted for, so adding a package here without also publishing its manifest
|
|
62
|
-
* relaxes the build rather than breaking it.
|
|
63
|
-
*/
|
|
64
|
-
export const LINK_PACKAGES = Object.freeze(["sohl", "thalorna"]);
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The **canonical** address of a note: fully qualified, one spelling per
|
|
68
|
-
* document, and globally unique.
|
|
69
|
-
*
|
|
70
|
-
* The written form of a link may omit the package (`[[skill-lang]]`), which
|
|
71
|
-
* defaults it to the citing note's own. Everything internal — index keys,
|
|
72
|
-
* manifest keys, every lookup — uses this instead, so no consumer has to know
|
|
73
|
-
* what a short form defaulted to.
|
|
74
|
-
*
|
|
75
|
-
* Global uniqueness is what lets a foreign manifest merge straight into a local
|
|
76
|
-
* index: the keys cannot collide by accident, so a key already present on merge
|
|
77
|
-
* is a real conflict rather than an artefact of two packages sharing a
|
|
78
|
-
* namespace. `(type, shortcode)` alone is unique only *within* a package, and
|
|
79
|
-
* two independently authored packages reaching for the same short string is a
|
|
80
|
-
* matter of time (#1499).
|
|
81
|
-
*
|
|
82
|
-
* @param {string} pkg - The owning **content** package (`sohl`, `thalorna`) —
|
|
83
|
-
* not the Foundry package, which varies per compilation target.
|
|
84
|
-
* @param {string} type - The note's `type`.
|
|
85
|
-
* @param {string} shortcode - The note's `shortcode`.
|
|
86
|
-
* @returns {string} `package/type/shortcode`, lowercased.
|
|
87
|
-
*/
|
|
88
|
-
export function canonicalKey(pkg, type, shortcode) {
|
|
89
|
-
return `${pkg}-${type}-${shortcode}`.toLowerCase();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* How many segments a canonical key has, and therefore how many the reader
|
|
94
|
-
* below counts.
|
|
95
|
-
*
|
|
96
|
-
* Named rather than written as a literal because it is the *grammar*, not an
|
|
97
|
-
* implementation detail of one function: it is the number a change to the
|
|
98
|
-
* address form would move, and the thing a reader of that change has to find.
|
|
99
|
-
*
|
|
100
|
-
* @type {number}
|
|
101
|
-
*/
|
|
102
|
-
export const CANONICAL_KEY_SEGMENTS = 3;
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Reads a canonical key back into its parts.
|
|
106
|
-
*
|
|
107
|
-
* Parsing is plain positional counting: split on the separator, require
|
|
108
|
-
* {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
|
|
109
|
-
* **The charset rule is what makes that sound** — every segment is
|
|
110
|
-
* `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
|
|
111
|
-
* so the hyphen is purely a separator and the count alone determines every
|
|
112
|
-
* field. That is enforced at each of the three sources rather than assumed of
|
|
113
|
-
* the data: shortcodes by `content-lint.mjs` (#1397), `contentPackage` by
|
|
114
|
-
* `defineConfig` (#59), and types are bare words. Were any of them free to
|
|
115
|
-
* carry a hyphen, no amount of counting would recover the fields and the reader
|
|
116
|
-
* would need a vocabulary to match against instead.
|
|
117
|
-
*
|
|
118
|
-
* **Nothing to read and nothing readable are different answers.** A key that
|
|
119
|
-
* cannot be canonical — `harn-adventures-skill-melee`, four segments — yields
|
|
120
|
-
* `null`, while an absent or blank input yields `undefined`. Both are falsy, so
|
|
121
|
-
* every call site (all of which test the result for truthiness) is unaffected;
|
|
122
|
-
* the distinction is there so a caller reporting "this key is unreadable" can
|
|
123
|
-
* tell that it has a key to report about.
|
|
124
|
-
*
|
|
125
|
-
* @param {unknown} key - A canonical key, or nothing.
|
|
126
|
-
* @returns {{package: string, type: string, shortcode: string}|null|undefined}
|
|
127
|
-
* The parts; `null` when there is a string that is not in canonical form;
|
|
128
|
-
* `undefined` when there is no key at all.
|
|
129
|
-
*/
|
|
130
|
-
export function readCanonicalKey(key) {
|
|
131
|
-
if (key == null || key === "") return undefined;
|
|
132
|
-
const parts = String(key).split("-");
|
|
133
|
-
if (parts.length !== CANONICAL_KEY_SEGMENTS) return null;
|
|
134
|
-
const [pkg, type, shortcode] = parts;
|
|
135
|
-
if (!pkg || !type || !shortcode) return null;
|
|
136
|
-
return { package: pkg, type, shortcode };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Manifest format version.
|
|
141
|
-
*
|
|
142
|
-
* Bumped to 2 by #1465: entries changed from a site-absolute `url` to a
|
|
143
|
-
* package-relative `path`. The two shapes are indistinguishable to a naive
|
|
144
|
-
* reader — prefixing a v1 `url` yields `/thalorna/thalorna/…`, which resolves,
|
|
145
|
-
* renders, and 404s — so the version is what makes a stale vendored file an
|
|
146
|
-
* error rather than a wrong link.
|
|
147
|
-
*
|
|
148
|
-
* Bumped to 4 by #1499: keys use the authored hyphen separator
|
|
149
|
-
* (`sohl-affliction-aconite`) so a key *is* the address an author writes; an
|
|
150
|
-
* item's documentation became an entry in its own right
|
|
151
|
-
* (`sohl-docaffliction-aconite`) rather than a second field; and entries gained
|
|
152
|
-
* `anchors`, mapping a note's named sections to the full UUID each compiled to.
|
|
153
|
-
*
|
|
154
|
-
* Bumped to 3 by #1499: keys became **canonical** — fully qualified
|
|
155
|
-
* `package/type/shortcode` rather than `type/shortcode` — and entries gained the
|
|
156
|
-
* Foundry `uuid` / `docUuid` beside the web `path`. A v2 key read as a v3 one
|
|
157
|
-
* addresses a package named after a type, so again the version is what turns a
|
|
158
|
-
* stale vendored file into an error.
|
|
159
|
-
*
|
|
160
|
-
* Bumped to 5 by #1516: `path` became optional, so a package that ships
|
|
161
|
-
* compendiums and publishes no site can still publish the Foundry addresses of
|
|
162
|
-
* its documents — the mirror of an entry that has a `path` and no `uuid`.
|
|
163
|
-
*/
|
|
164
|
-
export const MANIFEST_VERSION = 5;
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Every version this build can read, newest last.
|
|
168
|
-
*
|
|
169
|
-
* A version exists to stop a file whose values *read differently* from being
|
|
170
|
-
* resolved anyway, and that is the only thing it is allowed to gate. Every bump
|
|
171
|
-
* so far did change a reading — a v2 key read as a v4 one addresses a package
|
|
172
|
-
* named after a type — so each dropped its predecessors. **v5 did not**: it
|
|
173
|
-
* only permits an absent `path`, so every v4 value still means exactly what it
|
|
174
|
-
* meant, and refusing v4 would make a purely relaxing change a flag day in
|
|
175
|
-
* which every package must re-emit on the same afternoon or every build breaks
|
|
176
|
-
* (#1516).
|
|
177
|
-
*
|
|
178
|
-
* The unsafe direction is unchanged and still hard-fails: an older consumer
|
|
179
|
-
* meeting a newer file rejects it, because it cannot know what the new shape
|
|
180
|
-
* permits. Widening is therefore always safe to do here first and adopt
|
|
181
|
-
* elsewhere later.
|
|
182
|
-
*/
|
|
183
|
-
export const READABLE_VERSIONS = Object.freeze([4, MANIFEST_VERSION]);
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Where this build serves each package, keyed by package name.
|
|
187
|
-
*
|
|
188
|
-
* One line per package, and the only edit a relocation requires: point a
|
|
189
|
-
* package at another path (`"/setting/thalorna/"`) or another origin
|
|
190
|
-
* (`"https://thalorna.example.org/"`) and every inbound link into it follows.
|
|
191
|
-
* A base is a prefix, so it must end in `/`.
|
|
192
|
-
*
|
|
193
|
-
* Only *foreign* packages are consulted — a package this build publishes is
|
|
194
|
-
* authoritative in its own entries and never resolves through a manifest — but
|
|
195
|
-
* every linkable package is listed, because which are foreign depends on the
|
|
196
|
-
* consuming repository and this file is vendored into each of them.
|
|
197
|
-
*/
|
|
198
|
-
export const PACKAGE_BASE = Object.freeze({
|
|
199
|
-
sohl: "/sohl/",
|
|
200
|
-
thalorna: "/thalorna/",
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Asserts a base is usable as a prefix and returns it.
|
|
205
|
-
*
|
|
206
|
-
* @param {string} base - The package base.
|
|
207
|
-
* @param {string} what - What is being resolved, for the error message.
|
|
208
|
-
* @returns {string} The base.
|
|
209
|
-
*/
|
|
210
|
-
function checkBase(base, what) {
|
|
211
|
-
if (typeof base !== "string" || !base.endsWith("/")) {
|
|
212
|
-
throw new Error(`${what}: package base ${JSON.stringify(base)} must end in a slash`);
|
|
213
|
-
}
|
|
214
|
-
return base;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* The package-relative address a site-absolute URL records as.
|
|
219
|
-
*
|
|
220
|
-
* Strips the emitting package's own base, so what lands in the manifest says
|
|
221
|
-
* *where in the package* a page is and nothing about where the package itself
|
|
222
|
-
* is mounted. A URL outside the base is an error rather than a best effort: it
|
|
223
|
-
* would record an address that silently resolves to the wrong place once a
|
|
224
|
-
* consumer prefixes its own base.
|
|
225
|
-
*
|
|
226
|
-
* @param {string} url - The site-absolute URL the emitting build publishes at.
|
|
227
|
-
* @param {string} base - That build's base for the package, e.g. `"/thalorna/"`.
|
|
228
|
-
* @returns {string} The address relative to `base`, with no leading slash.
|
|
229
|
-
*/
|
|
230
|
-
export function packageRelative(url, base) {
|
|
231
|
-
checkBase(base, "packageRelative");
|
|
232
|
-
if (typeof url !== "string" || !url.startsWith(base)) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
`packageRelative: ${JSON.stringify(url)} does not sit under base ` +
|
|
235
|
-
`${JSON.stringify(base)}`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
return url.slice(base.length);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* The URL a package-relative address resolves to in this build.
|
|
243
|
-
*
|
|
244
|
-
* Plain concatenation, which is what makes an absolute-origin base work: a base
|
|
245
|
-
* of `"https://thalorna.example.org/"` yields an absolute link, and one of
|
|
246
|
-
* `"/thalorna/"` a root-relative one, with no other rule to keep in step.
|
|
247
|
-
*
|
|
248
|
-
* @param {string} rel - The package-relative address from a manifest entry.
|
|
249
|
-
* @param {string} base - This build's base for that package.
|
|
250
|
-
* @returns {string} The resolved URL.
|
|
251
|
-
*/
|
|
252
|
-
export function resolvePackageUrl(rel, base) {
|
|
253
|
-
checkBase(base, "resolvePackageUrl");
|
|
254
|
-
if (typeof rel !== "string" || !rel || rel.startsWith("/")) {
|
|
255
|
-
throw new Error(
|
|
256
|
-
`resolvePackageUrl: ${JSON.stringify(rel)} is not a package-` + `relative address`,
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
return `${base}${rel}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Builds one package's manifest from the KB build's own entries.
|
|
264
|
-
*
|
|
265
|
-
* Only notes carrying a `shortcode` appear: the shortcode is the stable
|
|
266
|
-
* identity another package addresses them by, and a note without one cannot be
|
|
267
|
-
* the target of a cross-package link at all.
|
|
268
|
-
*
|
|
269
|
-
* @param {string} pkg - The package name, e.g. `"sohl"`.
|
|
270
|
-
* @param {Array<object>} entries - KB entries (`{ fm, name, url }`).
|
|
271
|
-
* @param {string} [base] - Where *this* build serves `pkg`, stripped from each
|
|
272
|
-
* entry's URL so the recorded address is package-relative (#1465). Omitted,
|
|
273
|
-
* this build publishes no web surface for the package and no entry carries a
|
|
274
|
-
* `path` — see below.
|
|
275
|
-
* @param {string} [foundryPackage] - The Foundry package this build ships the
|
|
276
|
-
* compiled documents in. Given, each entry also carries the `uuid` /
|
|
277
|
-
* `docUuid` a pack build resolves against; omitted, the manifest describes
|
|
278
|
-
* the web surface only.
|
|
279
|
-
* @returns {object} The manifest document.
|
|
280
|
-
*/
|
|
281
|
-
export function buildManifest(pkg, entries, base, foundryPackage) {
|
|
282
|
-
// A base is what a `path` is recorded relative to, so having none is
|
|
283
|
-
// exactly the statement "this build publishes no pages for this package"
|
|
284
|
-
// (#1516). Making it a package-level decision the caller states once — not
|
|
285
|
-
// a per-note condition — is what stops a web-publishing package from
|
|
286
|
-
// half-emitting, where the notes that quietly lost a `path` would degrade
|
|
287
|
-
// to unlinked prose in every consumer with nothing erroring anywhere.
|
|
288
|
-
const web = base != null;
|
|
289
|
-
if (web) checkBase(base, `buildManifest(${pkg})`);
|
|
290
|
-
const out = {};
|
|
291
|
-
for (const e of entries) {
|
|
292
|
-
const type = e.fm?.type;
|
|
293
|
-
const shortcode = e.fm?.shortcode;
|
|
294
|
-
if (!type || typeof shortcode !== "string" || !shortcode) continue;
|
|
295
|
-
const entry = {
|
|
296
|
-
// The web address, for consumers rendering pages. Absent for a
|
|
297
|
-
// pack-only package, which has no page to point at — the mirror of
|
|
298
|
-
// the `uuid` case below, and stating a `path` anyway would assert a
|
|
299
|
-
// page that does not exist.
|
|
300
|
-
...(web ? { path: packageRelative(e.url, base) } : {}),
|
|
301
|
-
name: e.name,
|
|
302
|
-
};
|
|
303
|
-
// The Foundry address, for consumers compiling packs rather than pages.
|
|
304
|
-
// Supplied by the caller rather than derived here: only the build that
|
|
305
|
-
// splits a note into pages knows its anchors, and a note that compiles
|
|
306
|
-
// into no document has no UUID to state. Inventing one would assert a
|
|
307
|
-
// target that does not exist, so an entry without one is normal and a
|
|
308
|
-
// consumer must tolerate it.
|
|
309
|
-
if (foundryPackage && e.uuid) entry.uuid = e.uuid;
|
|
310
|
-
// The address of this item's documentation — a pointer to the entry
|
|
311
|
-
// that owns that UUID, not a second copy of it.
|
|
312
|
-
if (e.doc) entry.doc = e.doc;
|
|
313
|
-
// A note's named sections, each mapped to the *whole* UUID it compiled
|
|
314
|
-
// to. Whole, not a fragment appended to `uuid`: nothing owns a page
|
|
315
|
-
// address, so there is no fact being restated, and an anchor is not
|
|
316
|
-
// required to live inside its own entry. Publishing the complete link
|
|
317
|
-
// also keeps the page-id hash out of the published contract entirely.
|
|
318
|
-
if (e.anchors && Object.keys(e.anchors).length) entry.anchors = e.anchors;
|
|
319
|
-
out[e.key ?? canonicalKey(pkg, type, shortcode)] = entry;
|
|
320
|
-
}
|
|
321
|
-
return {
|
|
322
|
-
version: MANIFEST_VERSION,
|
|
323
|
-
package: pkg,
|
|
324
|
-
...(foundryPackage ? { foundryPackage } : {}),
|
|
325
|
-
// Sorted so the file is stable across builds and a diff shows only real
|
|
326
|
-
// change — it is committed by whoever vendors it.
|
|
327
|
-
entries: Object.fromEntries(
|
|
328
|
-
Object.entries(out).sort(([a], [b]) =>
|
|
329
|
-
a < b ? -1
|
|
330
|
-
: a > b ? 1
|
|
331
|
-
: 0,
|
|
332
|
-
),
|
|
333
|
-
),
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Writes one manifest per package into `dir`.
|
|
339
|
-
*
|
|
340
|
-
* @param {Map<string, Array<object>>} entriesByPackage - Package → entries.
|
|
341
|
-
* @param {string} dir - Output directory; created if absent.
|
|
342
|
-
* @param {Record<string, string>} bases - Package → where *this* build serves
|
|
343
|
-
* it, which is what each entry's address is recorded relative to. This is the
|
|
344
|
-
* emitting build's own layout, not {@link PACKAGE_BASE}: a package's own site
|
|
345
|
-
* commonly serves it at `"/"` while a consumer mounts it under a prefix.
|
|
346
|
-
* @param {Record<string, string>} [foundryPackages] - Package → the Foundry
|
|
347
|
-
* package shipping its documents. Only a package this build publishes can
|
|
348
|
-
* have one, since the UUID names where *this* repository ships them.
|
|
349
|
-
* @returns {Array<{ package: string, file: string, count: number }>} What was written.
|
|
350
|
-
*/
|
|
351
|
-
export function writeManifests(entriesByPackage, dir, bases, foundryPackages) {
|
|
352
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
353
|
-
const written = [];
|
|
354
|
-
for (const [pkg, entries] of entriesByPackage) {
|
|
355
|
-
const doc = buildManifest(pkg, entries, bases?.[pkg], foundryPackages?.[pkg]);
|
|
356
|
-
const file = path.join(dir, `${pkg}.json`);
|
|
357
|
-
fs.writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`);
|
|
358
|
-
written.push({
|
|
359
|
-
package: pkg,
|
|
360
|
-
file,
|
|
361
|
-
count: Object.keys(doc.entries).length,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
return written;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Loads vendored manifests for packages this build does not itself publish.
|
|
369
|
-
*
|
|
370
|
-
* A package built locally is skipped even if a manifest for it is present: the
|
|
371
|
-
* live build is authoritative and a vendored copy of it can only be stale.
|
|
372
|
-
*
|
|
373
|
-
* Each entry's package-relative address is resolved against this build's base
|
|
374
|
-
* for that package (#1465), so what the index holds is a usable `url` and every
|
|
375
|
-
* consumer downstream is unchanged by the format.
|
|
376
|
-
*
|
|
377
|
-
* @param {string} dir - Directory of vendored `<package>.json` manifests.
|
|
378
|
-
* @param {Iterable<string>} localPackages - Packages this build publishes.
|
|
379
|
-
* @param {Record<string, string>} [bases] - Package → base to resolve against;
|
|
380
|
-
* defaults to {@link PACKAGE_BASE}.
|
|
381
|
-
* @returns {{ index: Map<string, object>, packages: Set<string>, stale: Array<object> }}
|
|
382
|
-
* `index` maps the canonical `package-type-shortcode` → `{ url, name, uuid,
|
|
383
|
-
* doc, anchors, type, package }`. Keys are globally unique, so this merges
|
|
384
|
-
* directly into a local index with no prefixing and no separate lookup path.
|
|
385
|
-
* `url` is `undefined` for an entry with no page (#1516) and `uuid` for one
|
|
386
|
-
* that compiles into no document, so a caller must check the address it
|
|
387
|
-
* intends to use rather than assume a hit carries it.
|
|
388
|
-
*/
|
|
389
|
-
export function loadForeignManifests(dir, localPackages, bases = PACKAGE_BASE) {
|
|
390
|
-
const local = new Set(localPackages);
|
|
391
|
-
const index = new Map();
|
|
392
|
-
const packages = new Set();
|
|
393
|
-
const stale = [];
|
|
394
|
-
let names;
|
|
395
|
-
try {
|
|
396
|
-
names = fs.readdirSync(dir);
|
|
397
|
-
} catch {
|
|
398
|
-
return { index, packages, stale };
|
|
399
|
-
}
|
|
400
|
-
for (const name of names) {
|
|
401
|
-
if (!name.endsWith(".json")) continue;
|
|
402
|
-
const pkg = path.basename(name, ".json");
|
|
403
|
-
if (local.has(pkg)) continue;
|
|
404
|
-
let doc;
|
|
405
|
-
try {
|
|
406
|
-
doc = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
407
|
-
} catch (err) {
|
|
408
|
-
stale.push({ package: pkg, reason: `unreadable: ${err.message}` });
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
if (!READABLE_VERSIONS.includes(doc.version)) {
|
|
412
|
-
// A v1 file is the site-absolute shape (#1465). Prefixing one of
|
|
413
|
-
// its URLs would produce `/thalorna/thalorna/…` — a link that
|
|
414
|
-
// resolves here and 404s for the reader — so the mismatch has to
|
|
415
|
-
// stop the load rather than be resolved anyway.
|
|
416
|
-
stale.push({
|
|
417
|
-
package: pkg,
|
|
418
|
-
reason:
|
|
419
|
-
`manifest version ${doc.version}, expected one of ` +
|
|
420
|
-
`${READABLE_VERSIONS.join(", ")}`,
|
|
421
|
-
});
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
const entriesIn = Object.entries(doc.entries ?? {});
|
|
425
|
-
// A base is only needed to resolve a `path`, so a pack-only manifest —
|
|
426
|
-
// Foundry addresses and no pages (#1516) — needs none, and demanding
|
|
427
|
-
// one would make its documents uncitable from anywhere. Any entry that
|
|
428
|
-
// does carry a `path` brings the requirement straight back: dropping
|
|
429
|
-
// the package silently would turn every link into it back into an
|
|
430
|
-
// unresolved address, which reads as a typo far from the cause.
|
|
431
|
-
const base = bases?.[pkg];
|
|
432
|
-
const needsBase = entriesIn.some(([, v]) => v?.path != null);
|
|
433
|
-
if (needsBase && (typeof base !== "string" || !base)) {
|
|
434
|
-
stale.push({
|
|
435
|
-
package: pkg,
|
|
436
|
-
reason: `no package base configured for "${pkg}" (PACKAGE_BASE in packages/content-build/engine/kb-manifest.mjs)`,
|
|
437
|
-
});
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
const resolved = [];
|
|
441
|
-
try {
|
|
442
|
-
for (const [key, v] of entriesIn) {
|
|
443
|
-
// The type is read back out of the canonical key so a consumer
|
|
444
|
-
// can recognise a foreign package's types as addresses at all.
|
|
445
|
-
const type = readCanonicalKey(key)?.type;
|
|
446
|
-
resolved.push([
|
|
447
|
-
key,
|
|
448
|
-
{
|
|
449
|
-
name: v.name,
|
|
450
|
-
// Absent for an entry with no page. A consumer must
|
|
451
|
-
// tolerate that rather than invent an href, exactly as
|
|
452
|
-
// it already tolerates an entry with no `uuid`.
|
|
453
|
-
url: v.path == null ? undefined : resolvePackageUrl(v.path, base),
|
|
454
|
-
uuid: v.uuid,
|
|
455
|
-
doc: v.doc,
|
|
456
|
-
anchors: v.anchors,
|
|
457
|
-
type,
|
|
458
|
-
},
|
|
459
|
-
]);
|
|
460
|
-
}
|
|
461
|
-
} catch (err) {
|
|
462
|
-
stale.push({ package: pkg, reason: err.message });
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
packages.add(pkg);
|
|
466
|
-
for (const [key, v] of resolved) {
|
|
467
|
-
// First writer wins, so two packages claiming one address cannot
|
|
468
|
-
// make the build depend on directory order.
|
|
469
|
-
if (!index.has(key)) index.set(key, { ...v, package: pkg });
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return { index, packages, stale };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Whether every linkable package is accounted for, locally or by manifest.
|
|
477
|
-
*
|
|
478
|
-
* This is what gates the dead-link guard. It is deliberately derived from data
|
|
479
|
-
* rather than set by a flag: the guard turns itself on the moment the last
|
|
480
|
-
* missing manifest appears, instead of waiting for someone to remember.
|
|
481
|
-
*
|
|
482
|
-
* @param {Iterable<string>} localPackages - Packages this build publishes.
|
|
483
|
-
* @param {Iterable<string>} manifestPackages - Packages loaded from manifests.
|
|
484
|
-
* @returns {{ complete: boolean, missing: Array<string> }}
|
|
485
|
-
*/
|
|
486
|
-
export function manifestsComplete(localPackages, manifestPackages) {
|
|
487
|
-
const have = new Set([...localPackages, ...manifestPackages]);
|
|
488
|
-
const missing = LINK_PACKAGES.filter((p) => !have.has(p));
|
|
489
|
-
return { complete: missing.length === 0, missing };
|
|
490
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Every foreign package whose manifest entries a build cannot address.
|
|
3
|
-
*
|
|
4
|
-
* A package is reported only when it contributes entries and **none** of them
|
|
5
|
-
* yields a readable canonical key — the total, silent failure described above.
|
|
6
|
-
* Partial drift is deliberately not reported here: it resolves something, and
|
|
7
|
-
* whatever it fails to resolve surfaces as an ordinary dead address, pointed at
|
|
8
|
-
* the note that cites it. A package contributing no entries at all is likewise
|
|
9
|
-
* not a finding; a pack-only package publishes no addressable pages by design
|
|
10
|
-
* (#1516), and one being brought up publishes nothing yet.
|
|
11
|
-
*
|
|
12
|
-
* @param {Map<string, {package?: string}>} foreignIndex - `foreign.index` as
|
|
13
|
-
* returned by `loadForeignManifests`, keyed by canonical key.
|
|
14
|
-
* @returns {Array<{package: string, entries: number, sampleKey: string}>} One
|
|
15
|
-
* finding per drifted package, in the order the index first names each.
|
|
16
|
-
*/
|
|
17
|
-
export function unaddressableForeignPackages(foreignIndex: Map<string, {
|
|
18
|
-
package?: string;
|
|
19
|
-
}>): Array<{
|
|
20
|
-
package: string;
|
|
21
|
-
entries: number;
|
|
22
|
-
sampleKey: string;
|
|
23
|
-
}>;
|
|
24
|
-
/**
|
|
25
|
-
* One finding, in the standard `file:line:column: severity: message` form.
|
|
26
|
-
*
|
|
27
|
-
* The position is recovered by locating the offending key in the manifest text:
|
|
28
|
-
* the finding is about a literal the reader can see in the file, so its position
|
|
29
|
-
* is implicit rather than absent. When the file cannot be read, or the key is
|
|
30
|
-
* not in it, the locator degrades to the file alone — a dropped field, never a
|
|
31
|
-
* guessed `1:1` that would send the reader to the top of a 500 KB manifest for a
|
|
32
|
-
* finding that is not there.
|
|
33
|
-
*
|
|
34
|
-
* @param {{package: string, entries: number, sampleKey: string}} finding - One
|
|
35
|
-
* finding from {@link unaddressableForeignPackages}.
|
|
36
|
-
* @param {string} manifestDir - The directory the manifests were loaded from.
|
|
37
|
-
* @returns {string} The formatted diagnostic, path first on the line.
|
|
38
|
-
*/
|
|
39
|
-
export function formatUnaddressableFinding(finding: {
|
|
40
|
-
package: string;
|
|
41
|
-
entries: number;
|
|
42
|
-
sampleKey: string;
|
|
43
|
-
}, manifestDir: string): string;
|