@heroiclands/package-build 22.1.0 → 22.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/CONTENT.md +22 -0
- package/bin/content-build.mjs +29 -3
- package/bin/package-build.mjs +9 -26
- package/config.mjs +7 -2
- package/content-config.mjs +61 -2
- package/docs/api.md +26 -23
- package/docs/commands.md +22 -22
- package/docs/configuration.md +117 -54
- package/engine/content-links.mjs +19 -9
- package/engine/helpers.mjs +2 -1
- package/engine/metadata-index.mjs +32 -0
- package/engine/note-vocabulary.mjs +20 -0
- package/engine/pack-config.mjs +14 -1
- package/engine/schema-check.mjs +6 -6
- package/engine/site-build.mjs +14 -3
- package/engine/site-config.mjs +68 -27
- package/engine/site-index.mjs +10 -1
- package/engine/web-wikilinks.mjs +16 -6
- package/engine/wikilink-syntax.mjs +10 -0
- package/engine/wikilinks.mjs +34 -7
- package/manifest.mjs +16 -10
- package/package.json +1 -1
- package/release.mjs +41 -8
- package/types/config.d.mts +17 -0
- package/types/content-config.d.mts +11 -0
- package/types/engine/content-links.d.mts +3 -2
- package/types/engine/metadata-index.d.mts +22 -0
- package/types/engine/note-vocabulary.d.mts +12 -0
- package/types/engine/schema-check.d.mts +5 -5
- package/types/engine/site-build.d.mts +5 -1
- package/types/engine/site-config.d.mts +23 -12
- package/types/engine/site-index.d.mts +6 -1
- package/types/engine/web-wikilinks.d.mts +9 -5
- package/types/engine/wikilink-syntax.d.mts +3 -0
- package/types/engine/wikilinks.d.mts +16 -5
- package/types/manifest.d.mts +5 -3
- package/types/release.d.mts +7 -6
package/engine/site-config.mjs
CHANGED
|
@@ -44,7 +44,7 @@ import fs from "node:fs";
|
|
|
44
44
|
import path from "node:path";
|
|
45
45
|
import { stringify as stringifyToml } from "smol-toml";
|
|
46
46
|
|
|
47
|
-
import { checkHomepage } from "../config.mjs";
|
|
47
|
+
import { checkHomepage, fail } from "../config.mjs";
|
|
48
48
|
import { slugify } from "./content-slug.mjs";
|
|
49
49
|
|
|
50
50
|
/** The Hugo source directory, relative to the repository root. */
|
|
@@ -83,14 +83,39 @@ export const BRAND = Object.freeze({
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* The kinds
|
|
86
|
+
* The kinds a site with no tagged notes renders.
|
|
87
87
|
*
|
|
88
88
|
* A section exists only where `site.sections` declares one, so a tree holding
|
|
89
|
-
* only the homepage emits nothing beyond it;
|
|
90
|
-
* empty shells
|
|
89
|
+
* only the homepage emits nothing beyond it; a taxonomy nobody's notes fill
|
|
90
|
+
* and a feed would be empty shells. A site whose notes carry `tags:` emits
|
|
91
|
+
* `taxonomy` and `term` after all — see {@link hugoConfig} — but `RSS` is
|
|
92
|
+
* disabled either way: nothing here publishes a feed.
|
|
91
93
|
*/
|
|
92
94
|
export const DISABLE_KINDS = Object.freeze(["taxonomy", "term", "RSS"]);
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The kinds a site with at least one tagged note renders — everything but
|
|
98
|
+
* `RSS`.
|
|
99
|
+
*/
|
|
100
|
+
const DISABLE_KINDS_TAGGED = Object.freeze(["RSS"]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The single taxonomy a tagged site declares.
|
|
104
|
+
*
|
|
105
|
+
* Only `tag` — Hugo's default pair also declares `category`, which nothing
|
|
106
|
+
* here authors and which would publish an empty `/categories/`.
|
|
107
|
+
*/
|
|
108
|
+
const TAXONOMIES = Object.freeze({ tag: "tags" });
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The taxonomy output formats a tagged site declares — `HTML` only, so no
|
|
112
|
+
* feed is produced for `/tags/` or a single tag.
|
|
113
|
+
*/
|
|
114
|
+
const TAXONOMY_OUTPUTS = Object.freeze({
|
|
115
|
+
taxonomy: Object.freeze(["HTML"]),
|
|
116
|
+
term: Object.freeze(["HTML"]),
|
|
117
|
+
});
|
|
118
|
+
|
|
94
119
|
/**
|
|
95
120
|
* The markup settings the toolchain's own output requires.
|
|
96
121
|
*
|
|
@@ -356,14 +381,17 @@ function deepMerge(base, overrides) {
|
|
|
356
381
|
*
|
|
357
382
|
* @param {object} options - The sources.
|
|
358
383
|
* @param {object} options.config - The resolved build configuration.
|
|
359
|
-
* @param {string} [options.description] - `package.json`'s `description`.
|
|
360
384
|
* @param {readonly NavigationEntry[]} options.navigation - The navigation.
|
|
361
385
|
* @param {string} options.themesDir - From {@link resolveThemesDir}.
|
|
386
|
+
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
387
|
+
* carries `tags:`, from {@link module:engine/site-build.buildSite}'s
|
|
388
|
+
* `hasTags`. Defaults to `false` — no tagged note, no taxonomy pages.
|
|
362
389
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
363
390
|
* @throws {TypeError} When `homepage` fails `checkHomepage`, or the
|
|
364
|
-
* configuration declares no `packageBuild.manifest.title
|
|
391
|
+
* configuration declares no `packageBuild.manifest.title`, no
|
|
392
|
+
* `site.description`, or no `site.assets`.
|
|
365
393
|
*/
|
|
366
|
-
export function hugoConfig({ config,
|
|
394
|
+
export function hugoConfig({ config, navigation, themesDir, hasTags = false }) {
|
|
367
395
|
checkHomepage(config.homepage, config.contentPackage);
|
|
368
396
|
|
|
369
397
|
const title = config.packageBuild?.manifest?.title;
|
|
@@ -373,10 +401,23 @@ export function hugoConfig({ config, description, navigation, themesDir }) {
|
|
|
373
401
|
"and the site's `title` reads from it.",
|
|
374
402
|
);
|
|
375
403
|
}
|
|
404
|
+
if (!config.site.description) {
|
|
405
|
+
fail(
|
|
406
|
+
"site.description",
|
|
407
|
+
'is not declared, and the site\'s `<meta name="description">` reads from it',
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (!config.site.assets) {
|
|
411
|
+
fail(
|
|
412
|
+
"site.assets",
|
|
413
|
+
"is not declared, and a site build needs one — it is the host every " +
|
|
414
|
+
"package's imagery is served from, and the theme resolves every " +
|
|
415
|
+
"relative asset against it",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
376
418
|
|
|
377
419
|
/** @type {Record<string, unknown>} */
|
|
378
|
-
const params = {};
|
|
379
|
-
if (typeof description === "string" && description.trim()) params.description = description;
|
|
420
|
+
const params = { description: config.site.description };
|
|
380
421
|
if (config.author?.name) params.author = config.author.name;
|
|
381
422
|
if (config.site.assets) params.cdnBaseURL = config.site.assets;
|
|
382
423
|
params.brand = { ...BRAND };
|
|
@@ -391,11 +432,18 @@ export function hugoConfig({ config, description, navigation, themesDir }) {
|
|
|
391
432
|
themesDir,
|
|
392
433
|
theme: THEME,
|
|
393
434
|
contentDir: path.posix.relative(HUGO_SOURCE, HUGO_CONTENT),
|
|
394
|
-
disableKinds: [...DISABLE_KINDS],
|
|
435
|
+
disableKinds: hasTags ? [...DISABLE_KINDS_TAGGED] : [...DISABLE_KINDS],
|
|
395
436
|
params,
|
|
396
437
|
markup: structuredClone(MARKUP),
|
|
397
438
|
menu: { main: menuEntries(navigation) },
|
|
398
439
|
};
|
|
440
|
+
if (hasTags) {
|
|
441
|
+
generated.taxonomies = { ...TAXONOMIES };
|
|
442
|
+
generated.outputs = {
|
|
443
|
+
taxonomy: [...TAXONOMY_OUTPUTS.taxonomy],
|
|
444
|
+
term: [...TAXONOMY_OUTPUTS.term],
|
|
445
|
+
};
|
|
446
|
+
}
|
|
399
447
|
return deepMerge(generated, config.site.hugo);
|
|
400
448
|
}
|
|
401
449
|
|
|
@@ -414,35 +462,28 @@ export function hugoToml(generated) {
|
|
|
414
462
|
);
|
|
415
463
|
}
|
|
416
464
|
|
|
417
|
-
/**
|
|
418
|
-
* `package.json`'s `description`, or `undefined` when it declares none.
|
|
419
|
-
*
|
|
420
|
-
* @param {string} rootDir - The repository root.
|
|
421
|
-
* @returns {string|undefined} The description.
|
|
422
|
-
*/
|
|
423
|
-
function packageDescription(rootDir) {
|
|
424
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
425
|
-
return typeof pkg.description === "string" ? pkg.description : undefined;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
465
|
/**
|
|
429
466
|
* The Hugo configuration, every source read from the repository.
|
|
430
467
|
*
|
|
431
|
-
* Reads
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* intact.
|
|
468
|
+
* Reads the cached navigation and the installed theme's location, and
|
|
469
|
+
* composes them with {@link hugoConfig}. Nothing is written, so a caller can
|
|
470
|
+
* run this before touching the output tree and fail with it intact.
|
|
435
471
|
*
|
|
436
472
|
* @param {object} config - The resolved build configuration.
|
|
473
|
+
* @param {object} [options] - Options.
|
|
474
|
+
* @param {boolean} [options.hasTags] - Whether any note the site build walked
|
|
475
|
+
* carries `tags:`. Defaults to `false`, so a caller generating the
|
|
476
|
+
* configuration before the walk (to fail fast on a missing source) gets the
|
|
477
|
+
* untagged shape; pass the site build's own `hasTags` once it is known.
|
|
437
478
|
* @returns {Record<string, any>} The configuration Hugo reads.
|
|
438
479
|
* @throws {Error} When any source is missing or wrong.
|
|
439
480
|
*/
|
|
440
|
-
export function generateHugoConfig(config) {
|
|
481
|
+
export function generateHugoConfig(config, { hasTags = false } = {}) {
|
|
441
482
|
return hugoConfig({
|
|
442
483
|
config,
|
|
443
|
-
description: packageDescription(config.rootDir),
|
|
444
484
|
navigation: readCachedNavigation(config),
|
|
445
485
|
themesDir: resolveThemesDir(config.rootDir),
|
|
486
|
+
hasTags,
|
|
446
487
|
});
|
|
447
488
|
}
|
|
448
489
|
|
package/engine/site-index.mjs
CHANGED
|
@@ -166,9 +166,16 @@ function mergeForeign(index, foreignIndex) {
|
|
|
166
166
|
* @param {Map<string, {package: string, type?: string}>} [options.foreignIndex]
|
|
167
167
|
* The merged index from `loadForeignIndexes`. Omit when the build publishes
|
|
168
168
|
* no cross-package links.
|
|
169
|
+
* @param {Set<string>} [options.noIndexPackages] - Packages declared
|
|
170
|
+
* `contentIndex: false` — a Foundry dependency only, with no fetched index.
|
|
171
|
+
* A link naming one fails naming the key, rather than reading as prose or an
|
|
172
|
+
* ordinary dead address.
|
|
169
173
|
* @returns {SiteIndex} The index, and what could not be addressed unambiguously.
|
|
170
174
|
*/
|
|
171
|
-
export function buildSiteIndex(
|
|
175
|
+
export function buildSiteIndex(
|
|
176
|
+
entries,
|
|
177
|
+
{ foreignIndex = new Map(), noIndexPackages = new Set() } = {},
|
|
178
|
+
) {
|
|
172
179
|
const index = new Map();
|
|
173
180
|
const contentTypes = new Set();
|
|
174
181
|
const sections = new Set();
|
|
@@ -298,6 +305,7 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
298
305
|
contentTypes,
|
|
299
306
|
sections,
|
|
300
307
|
packages,
|
|
308
|
+
noIndexPackages,
|
|
301
309
|
refIndex,
|
|
302
310
|
conflicts,
|
|
303
311
|
};
|
|
@@ -347,6 +355,7 @@ export function wikiContext(
|
|
|
347
355
|
sections: built.sections,
|
|
348
356
|
contentTypes: built.contentTypes,
|
|
349
357
|
packages: built.packages,
|
|
358
|
+
noIndexPackages: built.noIndexPackages,
|
|
350
359
|
// The package a link written on this page defaults to when it names
|
|
351
360
|
// none. Taken from the resolved configuration, the same source
|
|
352
361
|
// the index's own addresses are built from, so a bare link cannot
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -279,9 +279,10 @@ function isPlainMap(value) {
|
|
|
279
279
|
*
|
|
280
280
|
* **Every target that resolves nowhere fails the build**, and is
|
|
281
281
|
* classified into the vocabulary all three resolvers share — `unlabelled`,
|
|
282
|
-
* `not-an-address`, `unknown-type`, `ambiguous`, `unresolved
|
|
283
|
-
* collected in `ctx.errors`, each carrying
|
|
284
|
-
* `occurrence` so a caller can report the line and
|
|
282
|
+
* `not-an-address`, `unknown-type`, `ambiguous`, `unresolved`,
|
|
283
|
+
* `no-content-index`. Failures are collected in `ctx.errors`, each carrying
|
|
284
|
+
* the authored `link` and its `occurrence` so a caller can report the line and
|
|
285
|
+
* column it sits on.
|
|
285
286
|
*
|
|
286
287
|
* There is deliberately no exception letting a hyphen-form address through while
|
|
287
288
|
* any linkable package had no vendored manifest, since a real cross-package
|
|
@@ -312,9 +313,12 @@ function isPlainMap(value) {
|
|
|
312
313
|
*
|
|
313
314
|
* @param {string} body - The markdown body.
|
|
314
315
|
* @param {object} ctx - `{ index, assets, collide, sections, contentTypes,
|
|
315
|
-
* packages, foreign, type, errors, src, file }`.
|
|
316
|
+
* packages, noIndexPackages, foreign, type, errors, src, file }`.
|
|
316
317
|
* `packages` is every package an address may name, without which the leading
|
|
317
|
-
* package segment of a canonical address reads as an unknown type;
|
|
318
|
+
* package segment of a canonical address reads as an unknown type;
|
|
319
|
+
* `noIndexPackages` is every package declared `contentIndex: false`, so a
|
|
320
|
+
* qualified address naming one fails with `no-content-index` rather than
|
|
321
|
+
* `not-an-address`; `foreign`
|
|
318
322
|
* is the cross-package manifest index; `assets` is the address space an embed
|
|
319
323
|
* resolves against. `src` is the page's display
|
|
320
324
|
* path and `file` the source file a diagnostic should name — absent, `src`
|
|
@@ -402,7 +406,12 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
402
406
|
// The canonical separator has to be resolved, not merely
|
|
403
407
|
// recognised. `null` here means the target is not an address at all,
|
|
404
408
|
// which is a defect: there is no other namespace to try.
|
|
405
|
-
const read = readQualifier(
|
|
409
|
+
const read = readQualifier(
|
|
410
|
+
target,
|
|
411
|
+
ctx.contentTypes ?? new Set(),
|
|
412
|
+
ctx.packages,
|
|
413
|
+
ctx.noIndexPackages,
|
|
414
|
+
);
|
|
406
415
|
const rawKey = target.toLowerCase();
|
|
407
416
|
const hit =
|
|
408
417
|
lookupRead(ctx.index, read, ctx.contentPackage) ??
|
|
@@ -478,6 +487,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
478
487
|
// a key: a partial address has no single key to be non-null.
|
|
479
488
|
: (read && !read.reason) || siteAddress ? "unresolved"
|
|
480
489
|
: read?.reason === "unknown-type" ? "unknown-type"
|
|
490
|
+
: read?.reason === "no-content-index" ? "no-content-index"
|
|
481
491
|
// Every link is an address, and this is not one. Distinct from
|
|
482
492
|
// a dead address, because the fix is different: a name has to
|
|
483
493
|
// become an address, not be corrected.
|
|
@@ -184,6 +184,9 @@ export function unlabelledLinkMessage(target) {
|
|
|
184
184
|
* - `unresolved` — parses as an address, and nothing publishes it.
|
|
185
185
|
* - `ambiguous` — more than one package publishes the short address.
|
|
186
186
|
* - `unknown-anchor` — the address resolved, the `#section` it names did not.
|
|
187
|
+
* - `no-content-index` — the address names a package declared
|
|
188
|
+
* `contentIndex: false`, a Foundry dependency only, so no index was fetched
|
|
189
|
+
* for it to resolve against.
|
|
187
190
|
*
|
|
188
191
|
* @type {ReadonlySet<string>}
|
|
189
192
|
*/
|
|
@@ -197,6 +200,7 @@ export const LINK_FINDING_REASONS = Object.freeze(
|
|
|
197
200
|
"unresolved",
|
|
198
201
|
"ambiguous",
|
|
199
202
|
"unknown-anchor",
|
|
203
|
+
"no-content-index",
|
|
200
204
|
]),
|
|
201
205
|
);
|
|
202
206
|
|
|
@@ -312,6 +316,12 @@ export function linkFindingMessage({ reason, target, packages, anchor, type }) {
|
|
|
312
316
|
);
|
|
313
317
|
case "unresolved":
|
|
314
318
|
return unresolvedAddressMessage(target);
|
|
319
|
+
case "no-content-index":
|
|
320
|
+
return (
|
|
321
|
+
`address [[${target}]] names a package declared \`contentIndex: false\` — ` +
|
|
322
|
+
`it is a Foundry dependency only, and no content index was fetched for it, ` +
|
|
323
|
+
`so nothing it publishes can be cited`
|
|
324
|
+
);
|
|
315
325
|
default:
|
|
316
326
|
throw new Error(
|
|
317
327
|
`linkFindingMessage: "${reason}" is not one of ` +
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -203,13 +203,18 @@ export function resolveItemDocType(qualifier, types) {
|
|
|
203
203
|
* @param {Set<string>} types - Every type the content tree contains.
|
|
204
204
|
* @param {Set<string>} [packages] - Every package an address may name. Omitted
|
|
205
205
|
* by callers that resolve within one package, where the form cannot occur.
|
|
206
|
+
* @param {Set<string>} [noIndexPackages] - Packages declared `contentIndex:
|
|
207
|
+
* false` — a Foundry dependency only, with no fetched index. A fully
|
|
208
|
+
* qualified target naming one is refused with `no-content-index` before its
|
|
209
|
+
* type is even considered, since there is no index to resolve it against.
|
|
206
210
|
* @returns {{type: string, shortcode: string, itemDoc: boolean,
|
|
207
211
|
* package?: string, system?: string, reason?: undefined}
|
|
208
|
-
* | {reason: "unknown-type"} | null}
|
|
212
|
+
* | {reason: "unknown-type"|"no-content-index", package?: string} | null}
|
|
209
213
|
* The resolved qualifier; a `reason` when the target is definitely qualified
|
|
210
|
-
* but names no known type; or `null` when it is not an
|
|
214
|
+
* but names no known type or no fetched index; or `null` when it is not an
|
|
215
|
+
* address at all.
|
|
211
216
|
*/
|
|
212
|
-
export function readQualifier(target, types, packages) {
|
|
217
|
+
export function readQualifier(target, types, packages, noIndexPackages) {
|
|
213
218
|
// **Package, system and type are lowercase; the shortcode is not.** A
|
|
214
219
|
// shortcode is case-sensitive and routinely mixed — `Clb`, `LtShoe`,
|
|
215
220
|
// `HsTunic` — so it is written as the note declares it. The three segments
|
|
@@ -221,7 +226,7 @@ export function readQualifier(target, types, packages) {
|
|
|
221
226
|
// (`[[Shock State]]`), and calling that a badly-cased address rather than
|
|
222
227
|
// not an address would name the wrong mistake. Neither tree carries a
|
|
223
228
|
// violation — 10,538 authored targets — so this pins a rule already kept.
|
|
224
|
-
const read = readQualifierCased(target, types, packages);
|
|
229
|
+
const read = readQualifierCased(target, types, packages, noIndexPackages);
|
|
225
230
|
if (read && !read.reason && qualifyingSegments(target).some((s) => /[A-Z]/.test(s))) {
|
|
226
231
|
return { reason: "not-lowercase" };
|
|
227
232
|
}
|
|
@@ -249,9 +254,10 @@ function qualifyingSegments(target) {
|
|
|
249
254
|
* @param {string} target
|
|
250
255
|
* @param {Set<string>} types
|
|
251
256
|
* @param {Set<string>} [packages]
|
|
257
|
+
* @param {Set<string>} [noIndexPackages]
|
|
252
258
|
* @returns {object|null}
|
|
253
259
|
*/
|
|
254
|
-
function readQualifierCased(target, types, packages) {
|
|
260
|
+
function readQualifierCased(target, types, packages, noIndexPackages) {
|
|
255
261
|
// The slash form is legacy and states neither package nor system, so it is
|
|
256
262
|
// read first and separately. A slash is unconditionally a qualifier —
|
|
257
263
|
// nothing else uses one — which is why an unknown type before it is
|
|
@@ -281,6 +287,10 @@ function readQualifierCased(target, types, packages) {
|
|
|
281
287
|
// qualified.
|
|
282
288
|
case 4: {
|
|
283
289
|
const pkg = norm(parts[0]);
|
|
290
|
+
// Checked before the type: a package with no fetched index has no
|
|
291
|
+
// vocabulary to resolve the rest of the target against, and the
|
|
292
|
+
// fix is the config declaration, not the shortcode.
|
|
293
|
+
if (noIndexPackages?.has(pkg)) return { reason: "no-content-index", package: pkg };
|
|
284
294
|
if (!packages?.has(pkg)) return null;
|
|
285
295
|
const system = norm(parts[1]);
|
|
286
296
|
if (!isSystemSegment(system)) return null;
|
|
@@ -359,11 +369,21 @@ export function anchorPageId(noteId, anchorSlug) {
|
|
|
359
369
|
* @param {Map<string, object>} [opts.assets] - The files this package ships, by
|
|
360
370
|
* canonical address. They resolve no link — an asset is not a document — and
|
|
361
371
|
* answer only the art fields, which name a file and never a document.
|
|
372
|
+
* @param {Set<string>} [opts.noIndexPackages] - Packages declared
|
|
373
|
+
* `contentIndex: false` — a Foundry dependency only. A link naming one fails
|
|
374
|
+
* with `no-content-index` rather than resolving, ambiguously, as either a
|
|
375
|
+
* typo or an undeclared package.
|
|
362
376
|
* @returns {{byShortcode: Map<string, object>, types: Set<string>}} `types` is
|
|
363
377
|
* every type the tree actually contains, so a qualifier naming no real type
|
|
364
378
|
* can be told apart from a missing target.
|
|
365
379
|
*/
|
|
366
|
-
export function buildWikilinkIndex(
|
|
380
|
+
export function buildWikilinkIndex(
|
|
381
|
+
docs,
|
|
382
|
+
packageId,
|
|
383
|
+
foreign,
|
|
384
|
+
contentPackage,
|
|
385
|
+
{ assets, noIndexPackages } = {},
|
|
386
|
+
) {
|
|
367
387
|
if (!packageId) {
|
|
368
388
|
throw new Error(
|
|
369
389
|
"buildWikilinkIndex: packageId is required — it is the first " +
|
|
@@ -454,6 +474,8 @@ export function buildWikilinkIndex(docs, packageId, foreign, contentPackage, { a
|
|
|
454
474
|
foreign: foreignByKey,
|
|
455
475
|
/** The files this package ships, by canonical address. */
|
|
456
476
|
assets: assets ?? new Map(),
|
|
477
|
+
/** Packages declared `contentIndex: false`, a Foundry dependency only. */
|
|
478
|
+
noIndexPackages: noIndexPackages ?? new Set(),
|
|
457
479
|
};
|
|
458
480
|
}
|
|
459
481
|
|
|
@@ -656,7 +678,12 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
656
678
|
if (target === "" && slug) {
|
|
657
679
|
doc = { type, id, pack, docPack };
|
|
658
680
|
} else {
|
|
659
|
-
const qualified = readQualifier(
|
|
681
|
+
const qualified = readQualifier(
|
|
682
|
+
target,
|
|
683
|
+
index.types,
|
|
684
|
+
index.packages,
|
|
685
|
+
index.noIndexPackages,
|
|
686
|
+
);
|
|
660
687
|
qualifiedRead = qualified;
|
|
661
688
|
// A target that does not parse as an address is a defect: there is
|
|
662
689
|
// no second namespace left to fall through to.
|
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.2.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/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
|
*
|
|
@@ -505,6 +505,17 @@ export type RelationshipSpec = {
|
|
|
505
505
|
* `_stats.systemVersion` is stamped from.
|
|
506
506
|
*/
|
|
507
507
|
compatibility?: CompatibilitySpec | undefined;
|
|
508
|
+
/**
|
|
509
|
+
* Whether `deps fetch` fetches this
|
|
510
|
+
* dependency's content index. Default
|
|
511
|
+
* `true`. `false` declares the dependency
|
|
512
|
+
* for the Foundry manifest only — nothing
|
|
513
|
+
* this tree cites by wikilink — and refuses
|
|
514
|
+
* `itemCatalog: true` on the same entry,
|
|
515
|
+
* since a catalogue is fetched from the same
|
|
516
|
+
* index.
|
|
517
|
+
*/
|
|
518
|
+
contentIndex?: boolean | undefined;
|
|
508
519
|
};
|
|
509
520
|
/**
|
|
510
521
|
* How a generated documentation page is framed in the repository publishing it.
|
|
@@ -144,8 +144,9 @@ export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Ar
|
|
|
144
144
|
* which addresses a foreign manifest answered. Each `deadAddresses` entry
|
|
145
145
|
* carries a `reason` from {@link LINK_FINDING_REASONS} —
|
|
146
146
|
* `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
|
|
147
|
-
* `packages`), or `"unresolved"` — and every one of
|
|
148
|
-
* the three resolvers agree on severity for every
|
|
147
|
+
* `packages`), `"no-content-index"`, or `"unresolved"` — and every one of
|
|
148
|
+
* them is an **error**: the three resolvers agree on severity for every
|
|
149
|
+
* class.
|
|
149
150
|
*/
|
|
150
151
|
export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
|
|
151
152
|
deadAnchors: object[];
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* and needing no items is the mirror of it. Gating the index on the catalogue
|
|
10
10
|
* flag would serve neither.
|
|
11
11
|
*
|
|
12
|
+
* **Excludes a relationship declaring `contentIndex: false`.** That opts a
|
|
13
|
+
* dependency out of both edges at once: it is a Foundry dependency only, cited
|
|
14
|
+
* by neither a wikilink nor an item reference, so there is nothing here for
|
|
15
|
+
* `deps fetch` to fill and no cache this build will ever read.
|
|
16
|
+
*
|
|
12
17
|
* The declaration is the one already in the emitted `system.json` /
|
|
13
18
|
* `module.json`, so it cannot drift from what Foundry itself installs, and
|
|
14
19
|
* there is no new configuration key to keep in step. Each entry carries the
|
|
@@ -25,6 +30,23 @@ export function metadataRelationships(config: object): Array<{
|
|
|
25
30
|
kind: string;
|
|
26
31
|
verified: string | undefined;
|
|
27
32
|
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Every package a relationship declares `contentIndex: false` on, keyed by
|
|
35
|
+
* the content package name a link into it would use.
|
|
36
|
+
*
|
|
37
|
+
* A separate set from {@link metadataRelationships}, which answers "what does
|
|
38
|
+
* `deps fetch` fill" — this answers "what does the link resolver recognise as
|
|
39
|
+
* a package with no fetched index", which a wikilink checker or pack compiler
|
|
40
|
+
* needs to tell that case apart from a package nobody declared at all.
|
|
41
|
+
*
|
|
42
|
+
* Walked across every relationship kind, not only the citable ones: the
|
|
43
|
+
* config validation refuses the flag nowhere by kind, so a resolver reading it
|
|
44
|
+
* back should not assume one either.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} config - The resolved build configuration.
|
|
47
|
+
* @returns {ReadonlySet<string>} The content package names.
|
|
48
|
+
*/
|
|
49
|
+
export function noContentIndexPackages(config: object): ReadonlySet<string>;
|
|
28
50
|
/**
|
|
29
51
|
* The cache directory for one dependency's index at one version.
|
|
30
52
|
*
|
|
@@ -50,6 +50,18 @@ export function exclusiveTagGroups(type: string, groups?: object): {
|
|
|
50
50
|
* @returns {boolean} Whether the note carries it.
|
|
51
51
|
*/
|
|
52
52
|
export function hasTag(fm: object | null | undefined, tag: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Whether a note carries any `tags:` at all, however authored.
|
|
55
|
+
*
|
|
56
|
+
* The one question the site build asks of tags in aggregate — whether the
|
|
57
|
+
* tree publishes taxonomy pages — rather than about a particular tag. Reads
|
|
58
|
+
* `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
|
|
59
|
+
* or a blank string as carrying none.
|
|
60
|
+
*
|
|
61
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
62
|
+
* @returns {boolean} Whether the note carries at least one tag.
|
|
63
|
+
*/
|
|
64
|
+
export function hasAnyTag(fm: object | null | undefined): boolean;
|
|
53
65
|
/**
|
|
54
66
|
* Whether a note is tagged as an unfinished **draft**.
|
|
55
67
|
*
|