@heroiclands/package-build 22.1.1 → 22.3.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 +18 -0
- package/CONTENT.md +27 -32
- package/bin/content-build.mjs +0 -3
- package/bin/package-build.mjs +9 -26
- package/config.mjs +7 -2
- package/content-config.mjs +73 -41
- package/docs/api.md +26 -25
- package/docs/commands.md +22 -22
- package/docs/configuration.md +78 -46
- package/docs/content-format.md +36 -6
- package/engine/foundry-entries.mjs +12 -1
- package/engine/frontmatter-lint.mjs +2 -3
- package/engine/helpers.mjs +4 -1
- package/engine/ids.mjs +12 -0
- package/engine/note-claims.mjs +21 -0
- package/engine/pack-config.mjs +14 -1
- package/engine/pack-router.mjs +82 -5
- package/engine/pdf-build.mjs +2 -2
- package/engine/retired-fields.mjs +2 -3
- package/engine/schema-check.mjs +6 -6
- package/engine/site-build.mjs +59 -218
- package/engine/site-config.mjs +22 -22
- package/engine/site-index.mjs +18 -62
- package/engine/web-wikilinks.mjs +2 -16
- package/engine/wikilinks.mjs +35 -14
- package/manifest.mjs +16 -10
- package/package.json +1 -1
- package/release.mjs +41 -8
- package/sohl/kb-passes.mjs +14 -85
- package/types/config.d.mts +17 -0
- package/types/content-config.d.mts +6 -6
- package/types/engine/ids.d.mts +11 -0
- package/types/engine/pack-router.d.mts +20 -2
- package/types/engine/schema-check.d.mts +5 -5
- package/types/engine/site-build.d.mts +12 -62
- package/types/engine/site-config.d.mts +6 -8
- package/types/engine/site-index.d.mts +3 -28
- package/types/engine/web-wikilinks.d.mts +1 -1
- package/types/engine/wikilinks.d.mts +6 -3
- package/types/manifest.d.mts +5 -3
- package/types/release.d.mts +7 -6
- package/types/sohl/kb-passes.d.mts +5 -36
package/engine/site-index.mjs
CHANGED
|
@@ -65,20 +65,14 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
65
65
|
*
|
|
66
66
|
* @typedef {object} SiteEntry
|
|
67
67
|
* @property {string} kind `"content"` for a note compiled from the content
|
|
68
|
-
* tree
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* tree. Only content entries take part in
|
|
69
|
+
* type-scoped indexing; anything else is carried
|
|
70
|
+
* through unindexed.
|
|
71
71
|
* @property {object} fm The note's frontmatter.
|
|
72
72
|
* @property {string} name Display name.
|
|
73
73
|
* @property {string} slug URL segment.
|
|
74
|
-
* @property {string} [sec] The Hugo section a **tree** page is filed under, and
|
|
75
|
-
* the first segment of the `<sec>/<slug>` address it
|
|
76
|
-
* is reachable by. A content page has none: it is
|
|
77
|
-
* addressed by `(type, shortcode)` and emitted flat.
|
|
78
74
|
* @property {string} base Source file's basename, e.g. `Climbing.md`.
|
|
79
75
|
* @property {string} url The page's published address.
|
|
80
|
-
* @property {boolean} [isReadme] Whether a tree page is its directory's
|
|
81
|
-
* landing.
|
|
82
76
|
*/
|
|
83
77
|
|
|
84
78
|
/**
|
|
@@ -94,23 +88,18 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
94
88
|
* from `index`.
|
|
95
89
|
* @property {Set<string>} contentTypes Every type the resolver should read as
|
|
96
90
|
* an address qualifier, local and foreign.
|
|
97
|
-
* @property {Set<string>} sections Section names, lowercased.
|
|
98
91
|
* @property {Map<string, {name: string, url: string, subType?: string}>} refIndex
|
|
99
92
|
* `type:shortcode` → page, for callers
|
|
100
93
|
* resolving embedded references (a
|
|
101
94
|
* being's items, say).
|
|
102
|
-
* @property {{key: string, package: string}[]} conflicts Addresses claimed by
|
|
103
|
-
* more than one package. Non-empty is a
|
|
104
|
-
* build failure; the caller reports it.
|
|
105
95
|
*/
|
|
106
96
|
|
|
107
97
|
/**
|
|
108
98
|
* Merge the packages this build does not publish into the local index.
|
|
109
99
|
*
|
|
110
100
|
* Every canonical key is globally unique, so a foreign manifest merges straight
|
|
111
|
-
* in — one map, one lookup, no precedence rule.
|
|
112
|
-
*
|
|
113
|
-
* canonical form exists to make detectable.
|
|
101
|
+
* in — one map, one lookup, no precedence rule. It runs before the local pass
|
|
102
|
+
* writes a single key, so a local page always ends up owning its own address.
|
|
114
103
|
*
|
|
115
104
|
* The short `type/shortcode` form is merged too, because a bare `[[doc-xyz]]`
|
|
116
105
|
* carries no package and must still find a foreign note when exactly one
|
|
@@ -122,22 +111,15 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
122
111
|
*
|
|
123
112
|
* @param {Map<string, object>} index - The local index, mutated.
|
|
124
113
|
* @param {Map<string, {package: string, type?: string}>} foreignIndex - Merged in.
|
|
125
|
-
* @returns {{
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* left out of the index, so a resolver can say *ambiguous* rather than
|
|
129
|
-
* *nothing answers*.
|
|
114
|
+
* @returns {{ambiguous: Set<string>}} The short `type/shortcode` forms two
|
|
115
|
+
* foreign packages claim — those are left out of the index, so a resolver
|
|
116
|
+
* can say *ambiguous* rather than *nothing answers*.
|
|
130
117
|
*/
|
|
131
118
|
function mergeForeign(index, foreignIndex) {
|
|
132
|
-
const conflicts = [];
|
|
133
119
|
const short = new Map();
|
|
134
120
|
const ambiguous = new Set();
|
|
135
121
|
|
|
136
122
|
for (const [key, value] of foreignIndex) {
|
|
137
|
-
if (index.has(key)) {
|
|
138
|
-
conflicts.push({ key, package: value.package });
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
123
|
index.set(key, value);
|
|
142
124
|
|
|
143
125
|
const parts = readCanonicalKey(key);
|
|
@@ -154,7 +136,7 @@ function mergeForeign(index, foreignIndex) {
|
|
|
154
136
|
for (const [key, value] of short) {
|
|
155
137
|
if (!index.has(key)) index.set(key, value);
|
|
156
138
|
}
|
|
157
|
-
return {
|
|
139
|
+
return { ambiguous };
|
|
158
140
|
}
|
|
159
141
|
|
|
160
142
|
/**
|
|
@@ -178,7 +160,6 @@ export function buildSiteIndex(
|
|
|
178
160
|
) {
|
|
179
161
|
const index = new Map();
|
|
180
162
|
const contentTypes = new Set();
|
|
181
|
-
const sections = new Set();
|
|
182
163
|
const refIndex = new Map();
|
|
183
164
|
// Every package an address may name: this build's own, plus every one a
|
|
184
165
|
// vendored manifest speaks for. Without it `readQualifier` cannot see the
|
|
@@ -187,30 +168,12 @@ export function buildSiteIndex(
|
|
|
187
168
|
const ownPackage = contentPackage();
|
|
188
169
|
const packages = new Set(ownPackage ? [ownPackage] : []);
|
|
189
170
|
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// A page's name, filename and bare slug were indexed here too, as
|
|
198
|
-
// collision-aware fallbacks the bare `[[Name]]` form looked up; that form is
|
|
199
|
-
// retired and nothing consults them, so they are gone and with them the rule
|
|
200
|
-
// that two pages of a type may not share a name.
|
|
201
|
-
for (const e of entries) {
|
|
202
|
-
if (typeof e.sec !== "string" || !e.sec) continue;
|
|
203
|
-
sections.add(e.sec.toLowerCase());
|
|
204
|
-
// `draft` rides on every key a page is addressable by, because a link
|
|
205
|
-
// into a draft note renders marked whichever of them the author wrote.
|
|
206
|
-
// It decides nothing about resolution: the page is indexed and
|
|
207
|
-
// published as any other.
|
|
208
|
-
index.set(`${e.sec}/${e.slug}`.toLowerCase(), {
|
|
209
|
-
url: e.url,
|
|
210
|
-
name: e.name,
|
|
211
|
-
draft: isDraftNote(e.fm),
|
|
212
|
-
});
|
|
213
|
-
}
|
|
171
|
+
// A page is addressed by `(type, shortcode)` and nothing else. Its name,
|
|
172
|
+
// filename and bare slug are not keys: the bare `[[Name]]` form that
|
|
173
|
+
// looked them up is retired, and with it the rule that two pages of a
|
|
174
|
+
// type may not share a name. A page carries no section either — a
|
|
175
|
+
// section is a listing the configuration declares, not part of any
|
|
176
|
+
// address.
|
|
214
177
|
|
|
215
178
|
// A foreign package may use a type this build has never seen. Seeding those
|
|
216
179
|
// is what lets the resolver recognise `polity-xyz` as an address at all —
|
|
@@ -226,15 +189,11 @@ export function buildSiteIndex(
|
|
|
226
189
|
// the local packages, so a manifest should never carry one — this is what
|
|
227
190
|
// makes that a belt-and-braces rather than the only thing standing between
|
|
228
191
|
// a stale vendored manifest and a shadowed local page.
|
|
229
|
-
|
|
230
|
-
// The corollary is that a conflict can only be reported against the keys
|
|
231
|
-
// that exist at this point — the addressing ones, `section/slug` and the
|
|
232
|
-
// bare fallbacks — which is precisely the overlap worth refusing.
|
|
233
|
-
const { conflicts, ambiguous } = mergeForeign(index, foreignIndex);
|
|
192
|
+
const { ambiguous } = mergeForeign(index, foreignIndex);
|
|
234
193
|
|
|
235
194
|
for (const e of entries) {
|
|
236
|
-
//
|
|
237
|
-
//
|
|
195
|
+
// Only a content note is addressable; anything else is carried
|
|
196
|
+
// through and takes no part in type-scoped indexing.
|
|
238
197
|
if (e.kind !== "content") continue;
|
|
239
198
|
const type = String(e.fm.type).toLowerCase();
|
|
240
199
|
contentTypes.add(type);
|
|
@@ -303,11 +262,9 @@ export function buildSiteIndex(
|
|
|
303
262
|
index,
|
|
304
263
|
ambiguous,
|
|
305
264
|
contentTypes,
|
|
306
|
-
sections,
|
|
307
265
|
packages,
|
|
308
266
|
noIndexPackages,
|
|
309
267
|
refIndex,
|
|
310
|
-
conflicts,
|
|
311
268
|
};
|
|
312
269
|
}
|
|
313
270
|
|
|
@@ -352,7 +309,6 @@ export function wikiContext(
|
|
|
352
309
|
// a URL and a name, an asset's carries the path to a file.
|
|
353
310
|
assets,
|
|
354
311
|
collide: built.ambiguous,
|
|
355
|
-
sections: built.sections,
|
|
356
312
|
contentTypes: built.contentTypes,
|
|
357
313
|
packages: built.packages,
|
|
358
314
|
noIndexPackages: built.noIndexPackages,
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -312,7 +312,7 @@ function isPlainMap(value) {
|
|
|
312
312
|
* for the website, and what the book reads its staging list out of.
|
|
313
313
|
*
|
|
314
314
|
* @param {string} body - The markdown body.
|
|
315
|
-
* @param {object} ctx - `{ index, assets, collide,
|
|
315
|
+
* @param {object} ctx - `{ index, assets, collide, contentTypes,
|
|
316
316
|
* packages, noIndexPackages, foreign, type, errors, src, file }`.
|
|
317
317
|
* `packages` is every package an address may name, without which the leading
|
|
318
318
|
* package segment of a canonical address reads as an unknown type;
|
|
@@ -415,10 +415,6 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
415
415
|
const rawKey = target.toLowerCase();
|
|
416
416
|
const hit =
|
|
417
417
|
lookupRead(ctx.index, read, ctx.contentPackage) ??
|
|
418
|
-
// `section/slug` is the site's own address for a page, and it is in
|
|
419
|
-
// the same map. Admitted only when the target carries a slash, so
|
|
420
|
-
// a page's bare slug cannot answer for an address.
|
|
421
|
-
(rawKey.includes("/") ? ctx.index.get(rawKey) : undefined) ??
|
|
422
418
|
// A manifest entry carries the same `{ url, name }` shape as a
|
|
423
419
|
// local one, so a cross-package hit needs no special case
|
|
424
420
|
// below. Local wins: a live build is authoritative and a vendored
|
|
@@ -449,16 +445,6 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
449
445
|
return hit.draft ? draftLink(link) : link;
|
|
450
446
|
}
|
|
451
447
|
|
|
452
|
-
const slash = target.indexOf("/");
|
|
453
|
-
const prefix = slash === -1 ? null : target.slice(0, slash).toLowerCase();
|
|
454
|
-
// A slash-qualified target whose prefix is a real KB **section** is an
|
|
455
|
-
// address in the site's own `section/slug` space, which the lookup
|
|
456
|
-
// above already consulted. It parses as no `type/shortcode`, but it did
|
|
457
|
-
// address something and nothing answered — so it is unresolved, not
|
|
458
|
-
// unaddressable. (A prefix that is a content *type* never reaches here:
|
|
459
|
-
// it parses as an address.)
|
|
460
|
-
const siteAddress = prefix !== null && ctx.sections.has(prefix);
|
|
461
|
-
|
|
462
448
|
// **An address resolving nowhere is a failure, unconditionally**.
|
|
463
449
|
//
|
|
464
450
|
// It was gated on a manifest-completeness check — while any linkable package was
|
|
@@ -485,7 +471,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
485
471
|
ctx.collide?.has(collideKey) ? "ambiguous"
|
|
486
472
|
// "It parsed as an address" is a property of the parse, not of
|
|
487
473
|
// a key: a partial address has no single key to be non-null.
|
|
488
|
-
:
|
|
474
|
+
: read && !read.reason ? "unresolved"
|
|
489
475
|
: read?.reason === "unknown-type" ? "unknown-type"
|
|
490
476
|
: read?.reason === "no-content-index" ? "no-content-index"
|
|
491
477
|
// Every link is an address, and this is not one. Distinct from
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -353,12 +353,14 @@ export function anchorPageId(noteId, anchorSlug) {
|
|
|
353
353
|
* Builds the link-resolution tables for a content tree.
|
|
354
354
|
*
|
|
355
355
|
* @param {Array<{type: string, id: string, shortcode?: string|null,
|
|
356
|
-
* name?: string, pack?: string, docPack?: string,
|
|
356
|
+
* name?: string, pack?: string, docPack?: string, none?: boolean,
|
|
357
357
|
* draft?: boolean}>} docs -
|
|
358
358
|
* One entry per content note. `pack` / `docPack` name the packs the note's
|
|
359
359
|
* document and its documentation entry landed in; omitted, the conventional
|
|
360
|
-
* one-pack-per-type names stand in. `
|
|
361
|
-
*
|
|
360
|
+
* one-pack-per-type names stand in. `none` says the note declares
|
|
361
|
+
* `pack: none` and compiles into no document, so it has no UUID to link to.
|
|
362
|
+
* `draft` says the note carries the `draft` tag, which marks links *into* it
|
|
363
|
+
* and changes nothing else.
|
|
362
364
|
* @param {string} packageId - The Foundry package shipping the packs; the first
|
|
363
365
|
* segment of every emitted UUID.
|
|
364
366
|
* @param {Map<string, object>} [foreign] - Canonically keyed entries from
|
|
@@ -408,17 +410,29 @@ export function buildWikilinkIndex(
|
|
|
408
410
|
if (!d.id || !d.type) continue;
|
|
409
411
|
types.add(norm(d.type));
|
|
410
412
|
|
|
411
|
-
uuidByDoc.set(
|
|
412
|
-
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
413
|
+
uuidByDoc.set(
|
|
414
|
+
d,
|
|
415
|
+
// A note declaring `pack: none` compiles into no document, so it
|
|
416
|
+
// has no address in any compendium: a link to it is a page on the
|
|
417
|
+
// web and prose in a journal. Recorded as an entry with no UUIDs
|
|
418
|
+
// rather than left out, so the address still resolves and a link
|
|
419
|
+
// to it is never reported as dead.
|
|
420
|
+
d.none ?
|
|
421
|
+
{ uuid: undefined, docUuid: undefined }
|
|
422
|
+
: {
|
|
423
|
+
// `d.pack` is where this note's document actually landed,
|
|
424
|
+
// resolved by the pack router when the index was
|
|
425
|
+
// collected. A repository may ship several packs of one
|
|
426
|
+
// type and a UUID carries the pack name, so the address
|
|
427
|
+
// cannot be derived from the type alone.
|
|
428
|
+
uuid: compendiumUuid(packageId, d.type, d.id, d.pack),
|
|
429
|
+
// An item's prose compiles into a separate JournalEntry,
|
|
430
|
+
// addressed by the virtual `doc<type>` qualifier. Its id
|
|
431
|
+
// is derived from the item's, so its address is knowable
|
|
432
|
+
// here too.
|
|
433
|
+
docUuid: compendiumUuid(packageId, "doc", itemDocEntryId(d.id), d.docPack),
|
|
434
|
+
},
|
|
435
|
+
);
|
|
422
436
|
|
|
423
437
|
if (d.shortcode) byShortcode.set(`${norm(d.type)}/${norm(d.shortcode)}`, d);
|
|
424
438
|
}
|
|
@@ -783,6 +797,13 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
783
797
|
docUuid: compendiumUuid(index.packageId, "doc", itemDocEntryId(doc.id), doc.docPack),
|
|
784
798
|
};
|
|
785
799
|
const entryUuid = itemDoc ? addresses.docUuid : addresses.uuid;
|
|
800
|
+
// The target is a note that compiles into no document — `pack: none`.
|
|
801
|
+
// The address is real and the page exists on the web, so this is not
|
|
802
|
+
// a dead link and must not fail the build; but a compendium has
|
|
803
|
+
// nothing to open, so the reader gets the prose and no `@UUID`. The
|
|
804
|
+
// mirror of what the website does for an address that publishes a
|
|
805
|
+
// document and no page.
|
|
806
|
+
if (entryUuid === undefined) return text;
|
|
786
807
|
const entryId = itemDoc ? itemDocEntryId(doc.id) : doc.id;
|
|
787
808
|
const isJournal = itemDoc || packForType(doc.type).docType === "JournalEntry";
|
|
788
809
|
// A JournalEntry link opens a journal — at its first page, or at the
|
package/manifest.mjs
CHANGED
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
*
|
|
29
29
|
* - **Declared** — the `packageBuild.manifest` block, emitted unchanged, so a
|
|
30
30
|
* key Foundry adds in a later version needs no release of this package.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* `descriptionHtml` is the one exception, folded into the derived
|
|
32
|
+
* `description` below rather than surviving under its own name.
|
|
33
|
+
* - **Derived** — the identity, the description (from `descriptionHtml`), the
|
|
34
|
+
* version, the release addresses, the compatibility ranges and the pack
|
|
35
|
+
* list. Declaring `description` directly is an error rather than an
|
|
36
|
+
* override: the authored copy would be silently overwritten.
|
|
35
37
|
* - **Computed** — namespaced `flags` a repository works out for itself.
|
|
36
38
|
*
|
|
37
39
|
* **Nothing here invents an address.** The repository URL is read from
|
|
@@ -459,11 +461,13 @@ function withoutBuildKeys(entry) {
|
|
|
459
461
|
* Three kinds of key end up in the result:
|
|
460
462
|
*
|
|
461
463
|
* - **Declared** — everything in `packageBuild.manifest`, emitted unchanged, so
|
|
462
|
-
* a key Foundry adds later needs no release of this package.
|
|
464
|
+
* a key Foundry adds later needs no release of this package. The one
|
|
465
|
+
* exception is `descriptionHtml`, folded into the description below rather
|
|
466
|
+
* than surviving under its own name.
|
|
463
467
|
* - **Derived** — the identity, the description, the release addresses, the
|
|
464
468
|
* version, the Foundry and system compatibility ranges, and the pack list.
|
|
465
|
-
* These are refused if also declared
|
|
466
|
-
* and the two would disagree with nothing to say so.
|
|
469
|
+
* These are refused if also declared (`description` directly; `descriptionHtml`
|
|
470
|
+
* is how it is authored) and the two would disagree with nothing to say so.
|
|
467
471
|
* - **Computed** — namespaced `flags` a repository works out for itself, merged
|
|
468
472
|
* over any it declared.
|
|
469
473
|
*
|
|
@@ -479,7 +483,9 @@ function withoutBuildKeys(entry) {
|
|
|
479
483
|
* @returns {object} The manifest, ready to serialise.
|
|
480
484
|
*/
|
|
481
485
|
export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
482
|
-
|
|
486
|
+
// `descriptionHtml` is the authored source of `description` — pulled out
|
|
487
|
+
// so it never survives the spread below under its own name.
|
|
488
|
+
const { descriptionHtml, ...declared } = config.packageBuild?.manifest ?? {};
|
|
483
489
|
const repoUrl = normalizeRepoUrl(packageJson.repository);
|
|
484
490
|
|
|
485
491
|
const derived = {
|
|
@@ -495,8 +501,8 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
|
495
501
|
};
|
|
496
502
|
// Own-property presence, not just value, decides whether a key survives
|
|
497
503
|
// into `ordered` below — an explicit `undefined` would still occupy a slot
|
|
498
|
-
// in it. Set only when
|
|
499
|
-
if (
|
|
504
|
+
// in it. Set only when the repository actually declares one.
|
|
505
|
+
if (descriptionHtml !== undefined) derived.description = descriptionHtml;
|
|
500
506
|
if (config.compatibility) derived.compatibility = config.compatibility;
|
|
501
507
|
|
|
502
508
|
// `requiresSystem` is the gate half of the declare/require split. It
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.3.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",
|
package/release.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The release archive — the two files a Foundry package's GitHub Release
|
|
16
|
-
* carries.
|
|
16
|
+
* carries, plus the optional siblings a repository opts into.
|
|
17
17
|
*
|
|
18
18
|
* Foundry installs a package by fetching the `download` URL its manifest
|
|
19
19
|
* advertises, so a release publishes `<artifact>.zip`, the whole staged tree,
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
* re-fetches to notice a new version. A package that ships content publishes a
|
|
22
22
|
* third: the content index other packages resolve its addresses through,
|
|
23
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`.
|
|
24
|
+
* by what the manifest says, not chosen here — see `manifest.mjs`. A package
|
|
25
|
+
* that publishes a DataModel schema (`packageBuild.schema`, staged through
|
|
26
|
+
* `packageBuild.assets`) gets a fourth: `schema.json`, published whenever the
|
|
27
|
+
* stage carries one and silently skipped otherwise.
|
|
25
28
|
*
|
|
26
29
|
* Kept apart from `stage.mjs` because this is the only part of assembling a
|
|
27
30
|
* package that needs a dependency. A repository that never cuts a release from
|
|
@@ -40,6 +43,8 @@ import path from "node:path";
|
|
|
40
43
|
// release job came to fail before a single byte was written.
|
|
41
44
|
import { ZipArchive } from "archiver";
|
|
42
45
|
|
|
46
|
+
import { SCHEMA_ARTIFACT_FILE } from "./engine/foreign-catalog.mjs";
|
|
47
|
+
|
|
43
48
|
/**
|
|
44
49
|
* Zip the staged tree and place the manifest beside the archive.
|
|
45
50
|
*
|
|
@@ -60,14 +65,14 @@ import { ZipArchive } from "archiver";
|
|
|
60
65
|
* @param {boolean} [opts.pdf] - Whether to build the book that ships beside the
|
|
61
66
|
* archive. `true` by default; `false` skips the build and reports the skip.
|
|
62
67
|
* @returns {Promise<{zip: string, manifest: string, metadata?: string,
|
|
63
|
-
* pdf?: string, pdfFindings: object[], pdfSkipped: string|null,
|
|
68
|
+
* schema?: string, pdf?: string, pdfFindings: object[], pdfSkipped: string|null,
|
|
64
69
|
* bytes: number, version: string}>} The paths written, what the book build
|
|
65
70
|
* found, the archive's size, and the version the manifest declares.
|
|
66
|
-
* `metadata` is absent when the manifest advertises no content index,
|
|
67
|
-
* `
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* `pdfFindings`.
|
|
71
|
+
* `metadata` is absent when the manifest advertises no content index,
|
|
72
|
+
* `schema` is absent when the stage carries no `schema.json`, and `pdf` is
|
|
73
|
+
* absent when no book was written. `pdfSkipped` is `null` when a book was
|
|
74
|
+
* built, and otherwise the reason none was — itself `null` when the book
|
|
75
|
+
* builder could not be loaded, which is reported through `pdfFindings`.
|
|
71
76
|
* @throws {Error} When the stage has no manifest — there is nothing to release,
|
|
72
77
|
* and an archive without one installs as nothing.
|
|
73
78
|
*/
|
|
@@ -119,6 +124,7 @@ export async function packRelease({
|
|
|
119
124
|
await fsp.copyFile(stagedManifest, path.join(out, manifestName));
|
|
120
125
|
|
|
121
126
|
const metadata = await publishMetadataIndex({ manifest, stage, out, metadataDir });
|
|
127
|
+
const schema = await publishSchemaAsset({ stage, out });
|
|
122
128
|
|
|
123
129
|
// Last, and never fatal: the archive and the manifest are the release, and
|
|
124
130
|
// a book that failed to set is a reported problem rather than a reason to
|
|
@@ -132,6 +138,7 @@ export async function packRelease({
|
|
|
132
138
|
zip: zipPath,
|
|
133
139
|
manifest: path.join(out, manifestName),
|
|
134
140
|
...(metadata ? { metadata } : {}),
|
|
141
|
+
...(schema ? { schema } : {}),
|
|
135
142
|
...(book.pdf ? { pdf: book.pdf } : {}),
|
|
136
143
|
pdfFindings: book.findings,
|
|
137
144
|
pdfSkipped: book.pdf ? null : book.reason,
|
|
@@ -226,3 +233,29 @@ async function publishMetadataIndex({ manifest, stage, out, metadataDir }) {
|
|
|
226
233
|
await fsp.copyFile(found, dest);
|
|
227
234
|
return dest;
|
|
228
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Place the published DataModel schema beside the archive, when the stage
|
|
239
|
+
* carries one.
|
|
240
|
+
*
|
|
241
|
+
* **The stage is the one place this looks.** `package-build schema` writes
|
|
242
|
+
* `build/schema.json`, and a repository that wants it released names it in
|
|
243
|
+
* `packageBuild.assets` (`from: build/schema.json`) the way every other
|
|
244
|
+
* staged file is declared — so a schema at the stage root is a repository
|
|
245
|
+
* that opted in, and its absence is a repository that has not, which is
|
|
246
|
+
* exactly as releasable as one that never adopted the artifact at all.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} opts
|
|
249
|
+
* @param {string} opts.stage - The staged tree.
|
|
250
|
+
* @param {string} opts.out - Where release assets are written.
|
|
251
|
+
* @returns {Promise<string|undefined>} The published path, or nothing when
|
|
252
|
+
* the stage carries no `schema.json`.
|
|
253
|
+
*/
|
|
254
|
+
async function publishSchemaAsset({ stage, out }) {
|
|
255
|
+
const src = path.join(stage, SCHEMA_ARTIFACT_FILE);
|
|
256
|
+
if (!fs.existsSync(src)) return undefined;
|
|
257
|
+
|
|
258
|
+
const dest = path.join(out, SCHEMA_ARTIFACT_FILE);
|
|
259
|
+
await fsp.copyFile(src, dest);
|
|
260
|
+
return dest;
|
|
261
|
+
}
|
package/sohl/kb-passes.mjs
CHANGED
|
@@ -12,25 +12,23 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The `sohl` knowledgebase's own body
|
|
15
|
+
* The `sohl` knowledgebase's own body pass.
|
|
16
16
|
*
|
|
17
17
|
* `content-build site` publishes a content tree as a website, and almost all of
|
|
18
|
-
* that job is the same for every package.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* that job is the same for every package. This rewrite is not: it is driven by
|
|
19
|
+
* a TypeDoc symbol map only this package has, and it is ruled explicitly
|
|
20
|
+
* per-consumer.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* It lives here rather than in a script in the consuming repository for the
|
|
23
23
|
* same reason `sohl/item-builders.mjs` and `sohl/being-info.mjs` do: a
|
|
24
24
|
* configuration is data and cannot hold a function, so a package-specific table
|
|
25
25
|
* of code is **named** from configuration and resolved from a registry. What is
|
|
26
26
|
* package-specific is the code; what is repository-specific — where the symbol
|
|
27
|
-
* map sits, what the API is served at
|
|
28
|
-
* options, supplied beside the name.
|
|
27
|
+
* map sits, what the API is served at — is options, supplied beside the name.
|
|
29
28
|
*
|
|
30
|
-
*
|
|
31
|
-
* degrades to a code span,
|
|
32
|
-
*
|
|
33
|
-
* broken link or a failed build for a syntax example in prose would not be.
|
|
29
|
+
* The *rewrite* never fails a build. A `{@link}` the map does not know
|
|
30
|
+
* degrades to a code span, which is legible to a reader; a failed build for a
|
|
31
|
+
* syntax example in prose would not be.
|
|
34
32
|
*
|
|
35
33
|
* Building the bundle is a different matter: a `symbolMap` that is configured
|
|
36
34
|
* and cannot be used fails, loudly, before a page is rendered. Degrading
|
|
@@ -168,80 +166,21 @@ export function resolveApiLinks(body, symbols, apiBase) {
|
|
|
168
166
|
});
|
|
169
167
|
}
|
|
170
168
|
|
|
171
|
-
/**
|
|
172
|
-
* Rewrites the relative links in a developer-doc body so they resolve on the
|
|
173
|
-
* published site.
|
|
174
|
-
*
|
|
175
|
-
* Developer docs are authored to link one another and the source tree with
|
|
176
|
-
* repository-relative paths, and neither target exists at the same path once
|
|
177
|
-
* rendered. Each link is resolved against the doc's own location:
|
|
178
|
-
*
|
|
179
|
-
* - a `*.md` link landing inside the documentation tree becomes the published
|
|
180
|
-
* route, preserving any `#anchor`; a `README` is its directory's landing, so
|
|
181
|
-
* that segment is dropped.
|
|
182
|
-
* - anything else — source, templates, a repository-root `*.md` — becomes its
|
|
183
|
-
* GitHub blob URL.
|
|
184
|
-
*
|
|
185
|
-
* Absolute URLs, anchor-only links, `mailto:` and site-root links are untouched.
|
|
186
|
-
*
|
|
187
|
-
* @param {string} body - The markdown body.
|
|
188
|
-
* @param {string} docRel - The doc's path relative to the documentation tree.
|
|
189
|
-
* @param {object} options - `{ repoRoot, docsSrc, docsRel, route, blob }`.
|
|
190
|
-
* @returns {string} The body with every relative link rewritten.
|
|
191
|
-
*/
|
|
192
|
-
export function rewriteRepoLinks(body, docRel, options) {
|
|
193
|
-
const { repoRoot, docsSrc, docsRel, route, blob } = options;
|
|
194
|
-
const docDir = path.dirname(docRel);
|
|
195
|
-
return body.replace(/\]\(([^)]+)\)/g, (whole, raw) => {
|
|
196
|
-
// Peel an optional link title: [text](url "title").
|
|
197
|
-
const sp = raw.search(/\s/);
|
|
198
|
-
const href = sp === -1 ? raw : raw.slice(0, sp);
|
|
199
|
-
const title = sp === -1 ? "" : raw.slice(sp);
|
|
200
|
-
if (/^(https?:|mailto:|tel:|#|\/)/.test(href)) return whole;
|
|
201
|
-
|
|
202
|
-
const hash = href.indexOf("#");
|
|
203
|
-
const filePart = hash === -1 ? href : href.slice(0, hash);
|
|
204
|
-
const anchor = hash === -1 ? "" : href.slice(hash);
|
|
205
|
-
if (!filePart) return whole;
|
|
206
|
-
|
|
207
|
-
const repoRel = path
|
|
208
|
-
.relative(repoRoot, path.resolve(docsSrc, docDir, filePart))
|
|
209
|
-
.replace(/\\/g, "/");
|
|
210
|
-
|
|
211
|
-
let out;
|
|
212
|
-
if (repoRel.startsWith(`${docsRel}/`) && repoRel.endsWith(".md")) {
|
|
213
|
-
const rel2 = repoRel.slice(docsRel.length + 1, -3).toLowerCase();
|
|
214
|
-
const devPath = path.basename(rel2) === "readme" ? path.posix.dirname(rel2) : rel2;
|
|
215
|
-
out = `${route}${devPath === "." ? "" : `${devPath}/`}${anchor}`;
|
|
216
|
-
} else {
|
|
217
|
-
out = `${blob}${repoRel}${anchor}`;
|
|
218
|
-
}
|
|
219
|
-
return `](${out}${title})`;
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
169
|
/**
|
|
224
170
|
* The `sohl` knowledgebase pass bundle, built from its options.
|
|
225
171
|
*
|
|
226
|
-
* A pass bundle is
|
|
227
|
-
* resolution,
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* `{@link}` tag may sit inside prose a wikilink also touches.
|
|
231
|
-
* - `afterLinks` runs only on pages from an **extra tree** — the documentation
|
|
232
|
-
* tree — because repository-relative links are a property of how those pages
|
|
233
|
-
* are authored, not of content notes.
|
|
234
|
-
*
|
|
235
|
-
* Both run inside code-fence protection, so neither can rewrite a fenced example.
|
|
172
|
+
* A pass bundle is the hook the page renderer calls before wikilink
|
|
173
|
+
* resolution: `beforeLinks` runs on every page, before wikilinks resolve,
|
|
174
|
+
* because a `{@link}` tag may sit inside prose a wikilink also touches. It
|
|
175
|
+
* runs inside code-fence protection, so it cannot rewrite a fenced example.
|
|
236
176
|
*
|
|
237
177
|
* @param {object} options - Resolved from `site.passOptions`.
|
|
238
178
|
* @param {string} [options.symbolMap] - Path to the TypeDoc symbol map,
|
|
239
179
|
* relative to `repoRoot`. Absent means no API links; present and unusable is
|
|
240
180
|
* a build failure.
|
|
241
181
|
* @param {string} [options.apiBase] - Where the API documentation is served.
|
|
242
|
-
* @param {string} [options.blob] - GitHub blob base for repository files.
|
|
243
182
|
* @param {string} options.repoRoot - The repository root, for relative paths.
|
|
244
|
-
* @returns {{beforeLinks: Function
|
|
183
|
+
* @returns {{beforeLinks: Function}} The bundle.
|
|
245
184
|
* @throws {Error} When a configured `symbolMap` cannot be resolved, read,
|
|
246
185
|
* parsed, or is not a name → page object.
|
|
247
186
|
*/
|
|
@@ -250,15 +189,5 @@ export function sohlKbPass(options) {
|
|
|
250
189
|
const apiBase = options.apiBase ?? "";
|
|
251
190
|
return {
|
|
252
191
|
beforeLinks: (text) => resolveApiLinks(text, symbols, apiBase),
|
|
253
|
-
afterLinks: (text, page) =>
|
|
254
|
-
page.tree ?
|
|
255
|
-
rewriteRepoLinks(text, page.rel, {
|
|
256
|
-
repoRoot: options.repoRoot,
|
|
257
|
-
docsSrc: page.tree.from,
|
|
258
|
-
docsRel: page.tree.rel,
|
|
259
|
-
route: page.tree.route,
|
|
260
|
-
blob: options.blob ?? "",
|
|
261
|
-
})
|
|
262
|
-
: text,
|
|
263
192
|
};
|
|
264
193
|
}
|
package/types/config.d.mts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reject a configured value, naming the key it was written under.
|
|
3
|
+
*
|
|
4
|
+
* The dotted path rides on the error as `field` as well as appearing in the
|
|
5
|
+
* message, so {@link loadPackageBuildConfig} — the half that knows which file
|
|
6
|
+
* was read — can resolve it to a line and column. This half stays pure.
|
|
7
|
+
*
|
|
8
|
+
* Exported so a sibling module composing a configuration value this module
|
|
9
|
+
* does not itself validate — {@link module:engine/site-config}'s
|
|
10
|
+
* `hugoConfig`, checking `site.assets` — reports through the one helper
|
|
11
|
+
* rather than a second copy.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} where - Dotted path of the offending key.
|
|
14
|
+
* @param {string} problem - What is wrong with it.
|
|
15
|
+
* @returns {never}
|
|
16
|
+
*/
|
|
17
|
+
export function fail(where: string, problem: string): never;
|
|
1
18
|
/**
|
|
2
19
|
* Where a declared `assetTransform` is loaded from.
|
|
3
20
|
*
|
|
@@ -133,10 +133,10 @@ export const DEFAULT_ADDRESS_SCHEME: Readonly<{
|
|
|
133
133
|
* and the default.
|
|
134
134
|
*
|
|
135
135
|
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
136
|
-
* is not walked for pages, `site.sections` / `site.
|
|
137
|
-
*
|
|
136
|
+
* is not walked for pages, `site.sections` / `site.landing` emit nothing,
|
|
137
|
+
* and nothing serves a page for its addresses.
|
|
138
138
|
* - `content` — the homepage *plus* every page the content tree publishes: the
|
|
139
|
-
* knowledgebase
|
|
139
|
+
* knowledgebase and the section landings.
|
|
140
140
|
*
|
|
141
141
|
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
142
142
|
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
@@ -200,10 +200,10 @@ export const DERIVED_HUGO_KEYS: Readonly<Record<string, string>>;
|
|
|
200
200
|
* and the default.
|
|
201
201
|
*
|
|
202
202
|
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
203
|
-
* is not walked for pages, `site.sections` / `site.
|
|
204
|
-
*
|
|
203
|
+
* is not walked for pages, `site.sections` / `site.landing` emit nothing,
|
|
204
|
+
* and nothing serves a page for its addresses.
|
|
205
205
|
* - `content` — the homepage *plus* every page the content tree publishes: the
|
|
206
|
-
* knowledgebase
|
|
206
|
+
* knowledgebase and the section landings.
|
|
207
207
|
*
|
|
208
208
|
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
209
209
|
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|