@heroiclands/package-build 5.0.0 → 6.1.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 +503 -0
- package/CONTENT.md +343 -16
- package/MIGRATING.md +41 -0
- package/README.md +30 -0
- package/bin/content-build.mjs +188 -4
- package/bin/package-build.mjs +3 -1
- package/content-config.mjs +29 -7
- package/engine/address-diff.mjs +290 -0
- package/engine/base-compiler.mjs +28 -3
- package/engine/content-links.mjs +221 -2
- package/engine/content-lint.mjs +35 -5
- package/engine/diagnostics.mjs +46 -0
- package/engine/frontmatter-lint.mjs +22 -0
- package/engine/generate.mjs +156 -7
- package/engine/homepage.mjs +315 -2
- package/engine/pack-config.mjs +21 -0
- package/engine/site-build.mjs +84 -23
- package/manifest.mjs +195 -0
- package/package.json +1 -1
- package/sohl/actors.mjs +14 -1
- package/sohl/item-fields.mjs +0 -5
- package/sohl/kb-passes.mjs +81 -14
- package/types/engine/address-diff.d.mts +108 -0
- package/types/engine/base-compiler.d.mts +22 -0
- package/types/engine/content-links.d.mts +53 -2
- package/types/engine/content-lint.d.mts +4 -1
- package/types/engine/diagnostics.d.mts +28 -0
- package/types/engine/generate.d.mts +50 -2
- package/types/engine/homepage.d.mts +166 -42
- package/types/engine/pack-config.d.mts +13 -0
- package/types/engine/site-build.d.mts +38 -7
- package/types/manifest.d.mts +67 -1
- package/types/sohl/kb-passes.d.mts +5 -1
package/manifest.mjs
CHANGED
|
@@ -49,8 +49,11 @@
|
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
51
|
import fs from "node:fs/promises";
|
|
52
|
+
import fsSync from "node:fs";
|
|
52
53
|
import path from "node:path";
|
|
53
54
|
|
|
55
|
+
import { emitDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
|
|
56
|
+
|
|
54
57
|
/**
|
|
55
58
|
* The two package kinds Foundry defines, as the artifact name each one's
|
|
56
59
|
* manifest and release archive are called.
|
|
@@ -197,6 +200,130 @@ export function manifestPacks(config) {
|
|
|
197
200
|
});
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Where a `packFolders` declaration lives in the configuration file.
|
|
205
|
+
*
|
|
206
|
+
* @type {readonly string[]}
|
|
207
|
+
*/
|
|
208
|
+
const PACK_FOLDERS_PATH = Object.freeze([
|
|
209
|
+
"packageBuild",
|
|
210
|
+
"manifest",
|
|
211
|
+
"packFolders",
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Every pack name a folder tree names, with the folder that named it.
|
|
216
|
+
*
|
|
217
|
+
* Foundry nests pack folders three deep — `PackageCompendiumFolder` re-declares
|
|
218
|
+
* itself while `depth < 4` — so a rule reading only the top level would miss
|
|
219
|
+
* every nested name in both directions: a broken one it never checked, and a
|
|
220
|
+
* working one it would then report as ungrouped.
|
|
221
|
+
*
|
|
222
|
+
* @param {unknown} folders - A `packFolders` list, or a nested `folders` list.
|
|
223
|
+
* @param {Array<string|number>} at - Config key path of `folders`.
|
|
224
|
+
* @returns {Array<{pack: string, folder: string, keyPath: Array<string|number>}>}
|
|
225
|
+
* One entry per named pack, in declaration order, depth first.
|
|
226
|
+
*/
|
|
227
|
+
function namedPacks(folders, at) {
|
|
228
|
+
if (!Array.isArray(folders)) return [];
|
|
229
|
+
const found = [];
|
|
230
|
+
folders.forEach((folder, index) => {
|
|
231
|
+
if (folder === null || typeof folder !== "object") return;
|
|
232
|
+
const name = String(folder.name ?? "");
|
|
233
|
+
const packs = Array.isArray(folder.packs) ? folder.packs : [];
|
|
234
|
+
packs.forEach((pack, position) => {
|
|
235
|
+
found.push({
|
|
236
|
+
pack: String(pack),
|
|
237
|
+
folder: name,
|
|
238
|
+
keyPath: [...at, index, "packs", position],
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
found.push(...namedPacks(folder.folders, [...at, index, "folders"]));
|
|
242
|
+
});
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* What `packFolders` and the derived `packs[]` disagree about.
|
|
248
|
+
*
|
|
249
|
+
* `packFolders` is the one **declared** manifest key that names something the
|
|
250
|
+
* build **derives**: every other declared key states a fact about the package
|
|
251
|
+
* (`title`, `socket`, `grid`) or addresses a staged file (`esmodules`,
|
|
252
|
+
* `styles`, `languages`), and a staged file is a different relation, checked
|
|
253
|
+
* against the stage rather than against configuration. So this is the one place
|
|
254
|
+
* a declaration can go stale against a value the build already computed — and
|
|
255
|
+
* until now nothing compared them (#81).
|
|
256
|
+
*
|
|
257
|
+
* `HarnMaster-3-FoundryVTT` shipped the consequence: its folder named four
|
|
258
|
+
* packs, three of which had not existed since the compendium was consolidated,
|
|
259
|
+
* and omitted `items` — 1,577 of 1,597 documents, loose in Foundry's compendium
|
|
260
|
+
* browser, with the build reporting nothing (HM3#420).
|
|
261
|
+
*
|
|
262
|
+
* **The two findings are not the same finding**, and giving them one severity
|
|
263
|
+
* gets one of them wrong:
|
|
264
|
+
*
|
|
265
|
+
* - _A folder names a pack that does not exist_ is an **error**. Foundry
|
|
266
|
+
* resolves the name against the package's own packs and silently skips what
|
|
267
|
+
* it cannot find, so the declaration does nothing at all; there is no
|
|
268
|
+
* arrangement in which it is intended, and the fix is unambiguous.
|
|
269
|
+
* - _A pack no folder names_ is a **warning**. It is legal and can be
|
|
270
|
+
* deliberate — a package may want one pack at the root — so failing on it
|
|
271
|
+
* would break working packages for a matter of taste. But a package that
|
|
272
|
+
* bothered to declare a folder rarely meant to leave one out, which is
|
|
273
|
+
* exactly how HM3's `items` went unnoticed.
|
|
274
|
+
* - _A package declaring no folders_ says **nothing**. Everything at the root
|
|
275
|
+
* is the majority arrangement, not an omission.
|
|
276
|
+
*
|
|
277
|
+
* Errors come first, in declaration order, then warnings in pack order: the
|
|
278
|
+
* unresolvable names are what a reader fixes, and a folder gaining a name often
|
|
279
|
+
* settles a warning too.
|
|
280
|
+
*
|
|
281
|
+
* @param {object} options
|
|
282
|
+
* @param {unknown} [options.packFolders] - The declared `packFolders`.
|
|
283
|
+
* @param {ReadonlyArray<{name: string}>} [options.packs] - The derived packs,
|
|
284
|
+
* as {@link manifestPacks} returns them.
|
|
285
|
+
* @returns {Array<{severity: "error"|"warning", message: string, pack: string,
|
|
286
|
+
* folder?: string, keyPath: Array<string|number>}>} The findings, ordered.
|
|
287
|
+
*/
|
|
288
|
+
export function packFolderFindings({ packFolders, packs = [] }) {
|
|
289
|
+
if (!Array.isArray(packFolders) || packFolders.length === 0) return [];
|
|
290
|
+
|
|
291
|
+
const shipped = packs.map((pack) => pack?.name).filter(Boolean);
|
|
292
|
+
const known = new Set(shipped);
|
|
293
|
+
const named = namedPacks(packFolders, PACK_FOLDERS_PATH);
|
|
294
|
+
const grouped = new Set(named.map((entry) => entry.pack));
|
|
295
|
+
|
|
296
|
+
const findings = named
|
|
297
|
+
.filter((entry) => !known.has(entry.pack))
|
|
298
|
+
.map((entry) => ({
|
|
299
|
+
severity: /** @type {const} */ ("error"),
|
|
300
|
+
pack: entry.pack,
|
|
301
|
+
folder: entry.folder,
|
|
302
|
+
keyPath: entry.keyPath,
|
|
303
|
+
message:
|
|
304
|
+
`packFolders: folder "${entry.folder}" names pack ` +
|
|
305
|
+
`"${entry.pack}", which this package does not ship ` +
|
|
306
|
+
`(packs: ${shipped.join(", ")})`,
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
for (const name of shipped) {
|
|
310
|
+
if (grouped.has(name)) continue;
|
|
311
|
+
findings.push({
|
|
312
|
+
severity: /** @type {const} */ ("warning"),
|
|
313
|
+
pack: name,
|
|
314
|
+
// No folder omitted it in particular — every one of them did — so
|
|
315
|
+
// the position is the declaration a reader edits, not one entry
|
|
316
|
+
// inside it. Each warning names its own pack, so they stay
|
|
317
|
+
// distinguishable despite sharing a line.
|
|
318
|
+
keyPath: [...PACK_FOLDERS_PATH],
|
|
319
|
+
message:
|
|
320
|
+
`packFolders: pack "${name}" is named by no folder, so it ` +
|
|
321
|
+
`ships outside every folder this package declares`,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return findings;
|
|
325
|
+
}
|
|
326
|
+
|
|
200
327
|
/**
|
|
201
328
|
* Relationship keys that direct the **build**, rather than describe the
|
|
202
329
|
* package.
|
|
@@ -330,16 +457,66 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
|
330
457
|
return ordered;
|
|
331
458
|
}
|
|
332
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Report what {@link packFolderFindings} found, and say whether it was fatal.
|
|
462
|
+
*
|
|
463
|
+
* The position comes from the configuration file, when one was named and can be
|
|
464
|
+
* read: a `packFolders` finding is about a line of YAML, and the file is the
|
|
465
|
+
* only place a line exists. Anything that cannot be established — an `.mjs`
|
|
466
|
+
* configuration, an unreadable file, a path that resolves to nothing — is
|
|
467
|
+
* dropped rather than guessed, so the diagnostic degrades from
|
|
468
|
+
* `file:line:column:` to `file:` to no locator at all.
|
|
469
|
+
*
|
|
470
|
+
* @param {ReturnType<typeof packFolderFindings>} findings - What was found.
|
|
471
|
+
* @param {string} [configFile] - Absolute path of the configuration file.
|
|
472
|
+
* @returns {number} How many of them were errors.
|
|
473
|
+
*/
|
|
474
|
+
function reportPackFolders(findings, configFile) {
|
|
475
|
+
if (!findings.length) return 0;
|
|
476
|
+
|
|
477
|
+
let text;
|
|
478
|
+
if (configFile) {
|
|
479
|
+
try {
|
|
480
|
+
text = fsSync.readFileSync(configFile, "utf8");
|
|
481
|
+
} catch {
|
|
482
|
+
text = undefined;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let errors = 0;
|
|
487
|
+
for (const finding of findings) {
|
|
488
|
+
if (finding.severity === "error") errors += 1;
|
|
489
|
+
emitDiagnostic({
|
|
490
|
+
...(configFile ? { file: configFile } : {}),
|
|
491
|
+
...(text ? positionOfYamlPath(text, finding.keyPath) : {}),
|
|
492
|
+
severity: finding.severity,
|
|
493
|
+
message: finding.message,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return errors;
|
|
497
|
+
}
|
|
498
|
+
|
|
333
499
|
/**
|
|
334
500
|
* Write the generated manifest into the staged package.
|
|
335
501
|
*
|
|
502
|
+
* The declared `packFolders` is checked against the derived `packs[]` first,
|
|
503
|
+
* and an unresolvable name **stops the write**: a manifest already known to
|
|
504
|
+
* describe packs the package does not ship should not reach the stage, where
|
|
505
|
+
* the next command would deploy it (#81). See {@link packFolderFindings} for
|
|
506
|
+
* the rule and why its two findings carry different severities.
|
|
507
|
+
*
|
|
336
508
|
* @param {object} options - As {@link buildManifest}, plus where to write.
|
|
337
509
|
* @param {object} options.config - The resolved content configuration.
|
|
338
510
|
* @param {object} options.packageJson - The repository's `package.json`.
|
|
339
511
|
* @param {string} options.artifact - `system` or `module`.
|
|
340
512
|
* @param {string} options.outDir - Directory to write into.
|
|
341
513
|
* @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
|
|
514
|
+
* @param {string} [options.configFile] - Absolute path of the configuration
|
|
515
|
+
* file the manifest was resolved from, so a finding about it can be located.
|
|
516
|
+
* Omitting it costs the position, not the finding.
|
|
342
517
|
* @returns {Promise<{path: string, manifest: object}>} Where it went, and what.
|
|
518
|
+
* @throws {Error} When a `packFolders` entry names a pack the package does not
|
|
519
|
+
* ship. Nothing is written in that case.
|
|
343
520
|
*/
|
|
344
521
|
export async function writeManifest({
|
|
345
522
|
config,
|
|
@@ -347,8 +524,26 @@ export async function writeManifest({
|
|
|
347
524
|
artifact,
|
|
348
525
|
outDir,
|
|
349
526
|
flags,
|
|
527
|
+
configFile,
|
|
350
528
|
}) {
|
|
351
529
|
const manifest = buildManifest({ config, packageJson, artifact, flags });
|
|
530
|
+
|
|
531
|
+
const errors = reportPackFolders(
|
|
532
|
+
packFolderFindings({
|
|
533
|
+
packFolders: manifest.packFolders,
|
|
534
|
+
packs: manifest.packs,
|
|
535
|
+
}),
|
|
536
|
+
configFile,
|
|
537
|
+
);
|
|
538
|
+
if (errors) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
`packFolders names ${errors} pack${errors === 1 ? "" : "s"} this ` +
|
|
541
|
+
`package does not ship (reported above). Foundry skips a name ` +
|
|
542
|
+
`it cannot resolve, so the folder would ship missing those ` +
|
|
543
|
+
`packs — correct \`packageBuild.manifest.packFolders\`.`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
352
547
|
await fs.mkdir(outDir, { recursive: true });
|
|
353
548
|
const outPath = path.join(outDir, `${artifact}.json`);
|
|
354
549
|
// Trailing newline: the file is committed to a release archive and read by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.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/sohl/actors.mjs
CHANGED
|
@@ -186,8 +186,16 @@ function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
|
|
|
186
186
|
const shadowed = [];
|
|
187
187
|
for (const itemsSourceDir of itemsSourceDirs) {
|
|
188
188
|
if (!fs.existsSync(itemsSourceDir)) {
|
|
189
|
+
// The generator orders the actors pass after every Item pass (#73),
|
|
190
|
+
// so a whole-package build cannot reach this. What can is a run
|
|
191
|
+
// restricted to this one pack, or a caller constructing the
|
|
192
|
+
// compiler itself — neither of which reordering a pack list fixes,
|
|
193
|
+
// so the message no longer suggests it.
|
|
189
194
|
throw new Error(
|
|
190
|
-
`Items source directory ${itemsSourceDir} does not exist —
|
|
195
|
+
`Items source directory ${itemsSourceDir} does not exist — ` +
|
|
196
|
+
`a being resolves its embedded items against the Item ` +
|
|
197
|
+
`packs' compiled output, so those packs must be compiled ` +
|
|
198
|
+
`before this one`,
|
|
191
199
|
);
|
|
192
200
|
}
|
|
193
201
|
for (const name of fs.readdirSync(itemsSourceDir)) {
|
|
@@ -310,6 +318,11 @@ export class Actors extends BasePackCompiler {
|
|
|
310
318
|
static id = "actors";
|
|
311
319
|
static label = "actor";
|
|
312
320
|
|
|
321
|
+
// A being's embedded items are resolved against the *output* of the item
|
|
322
|
+
// passes, so every Item pack compiles before this one. Declared rather than
|
|
323
|
+
// left to the order `packs:` happens to list (#73).
|
|
324
|
+
static readsPackOutputOf = Object.freeze(["Item"]);
|
|
325
|
+
|
|
313
326
|
/** @type {readonly string[]} */
|
|
314
327
|
itemsSourceDirs;
|
|
315
328
|
foreignSourceDirs;
|
package/sohl/item-fields.mjs
CHANGED
|
@@ -231,11 +231,6 @@ const GEAR_COMMON = Object.freeze([
|
|
|
231
231
|
value: true,
|
|
232
232
|
describe: "Whether it is being carried. Possession state.",
|
|
233
233
|
},
|
|
234
|
-
{
|
|
235
|
-
to: "isEquipped",
|
|
236
|
-
value: false,
|
|
237
|
-
describe: "Whether it is equipped. Possession state.",
|
|
238
|
-
},
|
|
239
234
|
]);
|
|
240
235
|
|
|
241
236
|
/* --------------------------------------------------------------------- */
|
package/sohl/kb-passes.mjs
CHANGED
|
@@ -27,35 +27,98 @@
|
|
|
27
27
|
* map sits, what the API is served at, which GitHub tree to link into — is
|
|
28
28
|
* options, supplied beside the name.
|
|
29
29
|
*
|
|
30
|
-
* Neither
|
|
31
|
-
* a code span, and a relative link that resolves outside the
|
|
32
|
-
* becomes a GitHub blob URL. Both are legible to a reader; a
|
|
33
|
-
* failed build for a syntax example in prose would not be.
|
|
30
|
+
* Neither *rewrite* ever fails a build. A `{@link}` the map does not know
|
|
31
|
+
* degrades to a code span, and a relative link that resolves outside the
|
|
32
|
+
* documentation tree becomes a GitHub blob URL. Both are legible to a reader; a
|
|
33
|
+
* broken link or a failed build for a syntax example in prose would not be.
|
|
34
|
+
*
|
|
35
|
+
* Building the bundle is a different matter: a `symbolMap` that is configured
|
|
36
|
+
* and cannot be used fails, loudly, before a page is rendered (#75). Degrading
|
|
37
|
+
* a tag the map does not know is a judgement about *prose*; degrading every tag
|
|
38
|
+
* on the site because the map was never read is a build that lied.
|
|
34
39
|
*
|
|
35
40
|
* @module
|
|
36
41
|
*/
|
|
37
42
|
|
|
38
43
|
import fs from "node:fs";
|
|
39
44
|
import path from "node:path";
|
|
45
|
+
import log from "loglevel";
|
|
40
46
|
|
|
41
47
|
/**
|
|
42
48
|
* Reads the TypeDoc symbol map, or an empty one.
|
|
43
49
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* than refuse to build.
|
|
50
|
+
* **Not configuring a map is the empty case, and it is silent.** The map is
|
|
51
|
+
* generated by `npm run docs`, and a knowledgebase build in a repository that
|
|
52
|
+
* has no API documentation should publish prose with `{@link}` tags degraded to
|
|
53
|
+
* code spans rather than refuse to build.
|
|
54
|
+
*
|
|
55
|
+
* **Configuring one that cannot be used is a defect, and it fails the build.**
|
|
56
|
+
* A missing file, a malformed one, a permissions error and a path typo used to
|
|
57
|
+
* be indistinguishable from each other *and* from a correctly configured build
|
|
58
|
+
* with no symbols: a bare `catch` returned `{}` for all five. Nothing then
|
|
59
|
+
* compares an emitted page against what its source asked for, so the first
|
|
60
|
+
* observer of a broken map was a reader who clicked nothing, because every
|
|
61
|
+
* `{@link}` on the published site had quietly become a code span (#75).
|
|
62
|
+
*
|
|
63
|
+
* The path is resolved against the **repository root**, never the process cwd.
|
|
64
|
+
* `site.passOptions.symbolMap` is authored repo-relative, so a cwd-relative
|
|
65
|
+
* read misses the moment `content-build site` is driven from anywhere but the
|
|
66
|
+
* repository root — which is exactly how #51's end-to-end verification, running
|
|
67
|
+
* through `PACKAGE_BUILD_CONFIG` from outside the tree, found this.
|
|
48
68
|
*
|
|
49
69
|
* @param {string|undefined} file - Path to the map, if configured.
|
|
70
|
+
* @param {string|undefined} repoRoot - The repository root to resolve against.
|
|
50
71
|
* @returns {Record<string, string>} Qualified name → API page path.
|
|
72
|
+
* @throws {Error} When a configured map cannot be resolved, read, parsed, or is
|
|
73
|
+
* not a name → page object.
|
|
51
74
|
*/
|
|
52
|
-
function readSymbolMap(file) {
|
|
75
|
+
function readSymbolMap(file, repoRoot) {
|
|
53
76
|
if (!file) return {};
|
|
77
|
+
|
|
78
|
+
// Refused rather than quietly resolved against the cwd, which is the
|
|
79
|
+
// defect this function was fixed for: a fallback would let it return.
|
|
80
|
+
if (!path.isAbsolute(file) && !repoRoot) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`site.passOptions.symbolMap ${JSON.stringify(file)} is relative ` +
|
|
83
|
+
`and no repoRoot was supplied to resolve it against`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const resolved = path.resolve(repoRoot ?? "", file);
|
|
87
|
+
|
|
88
|
+
let raw;
|
|
54
89
|
try {
|
|
55
|
-
|
|
56
|
-
} catch {
|
|
57
|
-
|
|
90
|
+
raw = fs.readFileSync(resolved, "utf8");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`site.passOptions.symbolMap ${resolved} cannot be read: ` +
|
|
94
|
+
`${err.message}`,
|
|
95
|
+
);
|
|
58
96
|
}
|
|
97
|
+
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(raw);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`site.passOptions.symbolMap ${resolved} is not valid JSON: ` +
|
|
104
|
+
`${err.message}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// `[]`, `"x"` and `null` all parse, and then every lookup misses — the same
|
|
109
|
+
// silent degradation reached a different way.
|
|
110
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`site.passOptions.symbolMap ${resolved} is not an object mapping ` +
|
|
113
|
+
`qualified names to API page paths`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Reported because a map that loaded and a map that loaded empty are
|
|
118
|
+
// otherwise indistinguishable without reading the emitted HTML — and an
|
|
119
|
+
// empty one degrades every tag exactly as a missing one used to.
|
|
120
|
+
log.info(`resolved ${Object.keys(parsed).length} API symbols from ${file}`);
|
|
121
|
+
return parsed;
|
|
59
122
|
}
|
|
60
123
|
|
|
61
124
|
/**
|
|
@@ -182,14 +245,18 @@ export function rewriteRepoLinks(body, docRel, options) {
|
|
|
182
245
|
* Both run inside code-fence protection, so neither can rewrite a fenced example.
|
|
183
246
|
*
|
|
184
247
|
* @param {object} options - Resolved from `site.passOptions`.
|
|
185
|
-
* @param {string} [options.symbolMap] - Path to the TypeDoc symbol map
|
|
248
|
+
* @param {string} [options.symbolMap] - Path to the TypeDoc symbol map,
|
|
249
|
+
* relative to `repoRoot`. Absent means no API links; present and unusable is
|
|
250
|
+
* a build failure.
|
|
186
251
|
* @param {string} [options.apiBase] - Where the API documentation is served.
|
|
187
252
|
* @param {string} [options.blob] - GitHub blob base for repository files.
|
|
188
253
|
* @param {string} options.repoRoot - The repository root, for relative paths.
|
|
189
254
|
* @returns {{beforeLinks: Function, afterLinks: Function}} The bundle.
|
|
255
|
+
* @throws {Error} When a configured `symbolMap` cannot be resolved, read,
|
|
256
|
+
* parsed, or is not a name → page object.
|
|
190
257
|
*/
|
|
191
258
|
export function sohlKbPass(options) {
|
|
192
|
-
const symbols = readSymbolMap(options.symbolMap);
|
|
259
|
+
const symbols = readSymbolMap(options.symbolMap, options.repoRoot);
|
|
193
260
|
const apiBase = options.apiBase ?? "";
|
|
194
261
|
return {
|
|
195
262
|
beforeLinks: (text) => resolveApiLinks(text, symbols, apiBase),
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The address space a set of compiled Item pack directories publishes.
|
|
3
|
+
*
|
|
4
|
+
* The directories are read as one space for the same reason the actors pass
|
|
5
|
+
* reads them as one: a being names an item by `(type, shortcode)` and never by
|
|
6
|
+
* the pack it happens to ship in. Both sides of a diff are built by this one
|
|
7
|
+
* function, so a released catalogue extracted by `deps fetch` and a freshly
|
|
8
|
+
* compiled pack are indexed identically and a difference between them is a real
|
|
9
|
+
* one rather than an artefact of two readers.
|
|
10
|
+
*
|
|
11
|
+
* A missing directory throws rather than reading as an empty space: an empty
|
|
12
|
+
* baseline would report every address in the package as withdrawn, and an empty
|
|
13
|
+
* current side would report every address as gone — the loudest possible
|
|
14
|
+
* output from the quietest possible mistake.
|
|
15
|
+
*
|
|
16
|
+
* @param {readonly string[]} dirs - Directories of item JSON.
|
|
17
|
+
* @returns {Map<string, {id: string, name: string, type: string, shortcode: string, file: string}>}
|
|
18
|
+
* Every item, keyed `type:shortcode`.
|
|
19
|
+
*/
|
|
20
|
+
export function readItemAddresses(dirs: readonly string[]): Map<string, {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
shortcode: string;
|
|
25
|
+
file: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Every address the baseline published that this build does not.
|
|
29
|
+
*
|
|
30
|
+
* An address that merely *arrived* is not a finding: adding one breaks nobody.
|
|
31
|
+
* The arrivals are read only to answer the one question that matters about a
|
|
32
|
+
* departure — is the document still here under another name?
|
|
33
|
+
*
|
|
34
|
+
* @param {Map<string, object>} baseline - The released address space.
|
|
35
|
+
* @param {Map<string, object>} current - This build's address space.
|
|
36
|
+
* @param {object} opts
|
|
37
|
+
* @param {string} opts.baseline - What the baseline is, for the message —
|
|
38
|
+
* conventionally `<package>@<version>`.
|
|
39
|
+
* @returns {Array<object>} One finding per departed address, in address order
|
|
40
|
+
* so two runs read the same. `kind` is `"renamed"` (with `to`) or
|
|
41
|
+
* `"withdrawn"`.
|
|
42
|
+
*/
|
|
43
|
+
export function diffItemAddresses(baseline: Map<string, object>, current: Map<string, object>, { baseline: label }: {
|
|
44
|
+
baseline: string;
|
|
45
|
+
}): Array<object>;
|
|
46
|
+
/**
|
|
47
|
+
* Every content note in a tree, indexed by the document id it authors.
|
|
48
|
+
*
|
|
49
|
+
* The address space is read from compiled output because that is what actually
|
|
50
|
+
* ships; the tree is read only to place a finding somewhere a reader can open
|
|
51
|
+
* and fix it. Each source answers the question it is good at, and the id is the
|
|
52
|
+
* exact key that joins them.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} contentBase - Root of the content tree.
|
|
55
|
+
* @param {object} [opts]
|
|
56
|
+
* @param {readonly string[]} [opts.skipDirectories] - Passed to the walk.
|
|
57
|
+
* @returns {Map<string, string>} Document id → the note's absolute path.
|
|
58
|
+
*/
|
|
59
|
+
export function noteFilesById(contentBase: string, { skipDirectories }?: {
|
|
60
|
+
skipDirectories?: readonly string[] | undefined;
|
|
61
|
+
}): Map<string, string>;
|
|
62
|
+
/**
|
|
63
|
+
* Where to send the reader for one finding.
|
|
64
|
+
*
|
|
65
|
+
* A rename is fixed in the note that made it, so a finding whose id is still in
|
|
66
|
+
* this tree is reported at that note's `shortcode:` line — the line the author
|
|
67
|
+
* just edited. A withdrawal has no such note by definition, so it degrades to
|
|
68
|
+
* the baseline document, which is the only artefact left that records the
|
|
69
|
+
* address existing. When neither is readable the position is **dropped**, never
|
|
70
|
+
* defaulted to `1:1`.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} finding - One finding from {@link diffItemAddresses}.
|
|
73
|
+
* @param {Map<string, string>} noteFiles - From {@link noteFilesById}.
|
|
74
|
+
* @returns {{file?: string, line?: number, column?: number}} Spreadable
|
|
75
|
+
* position fields for {@link formatDiagnostic}.
|
|
76
|
+
*/
|
|
77
|
+
export function locateAddressFinding(finding: object, noteFiles: Map<string, string>): {
|
|
78
|
+
file?: string;
|
|
79
|
+
line?: number;
|
|
80
|
+
column?: number;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* What one finding says, without a locator or a severity.
|
|
84
|
+
*
|
|
85
|
+
* The rename message names the identity it matched on, because that is what
|
|
86
|
+
* separates this from a spelling suggestion: the reader can check the id in
|
|
87
|
+
* both artefacts. The withdrawal message names no successor, because none is
|
|
88
|
+
* known — and says so, rather than leaving the reader to wonder whether one was
|
|
89
|
+
* looked for.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} finding - One finding from {@link diffItemAddresses}.
|
|
92
|
+
* @returns {string} The message.
|
|
93
|
+
*/
|
|
94
|
+
export function addressFindingMessage(finding: object): string;
|
|
95
|
+
/**
|
|
96
|
+
* One finding, in the standard `file:line:column: severity: message` form.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} finding - One finding from {@link diffItemAddresses}.
|
|
99
|
+
* @param {{file?: string, line?: number, column?: number}} at - From
|
|
100
|
+
* {@link locateAddressFinding}.
|
|
101
|
+
* @param {"warning"|"error"} [severity] - `error` when the caller is gating.
|
|
102
|
+
* @returns {string} The formatted diagnostic, path first on the line.
|
|
103
|
+
*/
|
|
104
|
+
export function formatAddressFinding(finding: object, at: {
|
|
105
|
+
file?: string;
|
|
106
|
+
line?: number;
|
|
107
|
+
column?: number;
|
|
108
|
+
}, severity?: "warning" | "error"): string;
|
|
@@ -57,6 +57,28 @@ export class BasePackCompiler {
|
|
|
57
57
|
* @type {boolean}
|
|
58
58
|
*/
|
|
59
59
|
static convertsWikilinks: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The document types whose **compiled output** this pass reads.
|
|
62
|
+
*
|
|
63
|
+
* Empty for every pass that reads only the content tree. The actors pass
|
|
64
|
+
* is the exception: a being names its embedded items by
|
|
65
|
+
* `(type, shortcode)`, and it resolves them against the JSON the item
|
|
66
|
+
* passes wrote — so an Actor pass must run after every Item pass, and it
|
|
67
|
+
* says so here.
|
|
68
|
+
*
|
|
69
|
+
* The generator derives the compile order from this (#73), so the order
|
|
70
|
+
* `packs:` declares is presentation only — it is the manifest's `packs`
|
|
71
|
+
* array as well, and a consumer orders that for a reader. A pass that
|
|
72
|
+
* reads another's output states the dependency once, in the class that
|
|
73
|
+
* does the reading, instead of every consuming repository having to know
|
|
74
|
+
* it when writing its pack list.
|
|
75
|
+
*
|
|
76
|
+
* A consumer registering a compiler of its own declares its dependencies
|
|
77
|
+
* the same way; a type no pack declares is simply not waited for.
|
|
78
|
+
*
|
|
79
|
+
* @type {readonly string[]}
|
|
80
|
+
*/
|
|
81
|
+
static readsPackOutputOf: readonly string[];
|
|
60
82
|
/**
|
|
61
83
|
* @param {object} options
|
|
62
84
|
* @param {string} options.contentBase - Root of the content tree.
|
|
@@ -26,18 +26,69 @@ export function buildLinkIndex(contentBase: string, { manifestDir, skipDirectori
|
|
|
26
26
|
manifestDir?: string | undefined;
|
|
27
27
|
skipDirectories?: readonly string[] | undefined;
|
|
28
28
|
}): object;
|
|
29
|
+
/**
|
|
30
|
+
* Every defect in the addresses a package homepage carries.
|
|
31
|
+
*
|
|
32
|
+
* **Why the homepage needs its own audit at all.** Every other note addresses
|
|
33
|
+
* the corpus with wikilinks, which {@link auditLinks} resolves. A homepage does
|
|
34
|
+
* not and cannot: it is published *verbatim* by every publishing mode, including
|
|
35
|
+
* the homepage-only mode two fan-licensed packages ship under, where the content
|
|
36
|
+
* tree is never walked and there is no index for a wikilink to resolve against.
|
|
37
|
+
* So a landing addresses the web the way the web does — markdown links and
|
|
38
|
+
* `url:` fields — and nothing was looking at those. SoHL's landing pointed at
|
|
39
|
+
* `kb/creature/` and `kb/character/` from the day those types merged into
|
|
40
|
+
* `being`: two 404s on the package's front page, through every build.
|
|
41
|
+
*
|
|
42
|
+
* **What is checkable, stated plainly.** Only an address into this site is, and
|
|
43
|
+
* only against facts this build already holds:
|
|
44
|
+
*
|
|
45
|
+
* - A **retired content type** in the path. The engine knows what used to exist
|
|
46
|
+
* and what replaced it, so this is a fact rather than a guess — and it is
|
|
47
|
+
* exactly the SoHL defect.
|
|
48
|
+
* - A **hardcoded absolute URL** into this package's own prefix, or into one a
|
|
49
|
+
* vendored manifest names. Both have a better form to write, which is why they
|
|
50
|
+
* are reported; a bare `/<package>/` is left alone, because a package
|
|
51
|
+
* homepage is in no manifest and there is nothing better to write.
|
|
52
|
+
* - A **root-relative `url:`**, which the theme's `relURL` prefixes a second
|
|
53
|
+
* time. `href:` means "already resolved, use verbatim", so the same leading
|
|
54
|
+
* slash is correct there and is not reported.
|
|
55
|
+
* - A **wikilink**, which nothing on this page will ever resolve.
|
|
56
|
+
*
|
|
57
|
+
* **What is not checkable, and is not attempted.** Whether an external URL
|
|
58
|
+
* answers — there is no network at build time, and a build must not fail because
|
|
59
|
+
* a third party is down. And whether a live in-site address names a page that
|
|
60
|
+
* exists: several of the surfaces a landing routes to are produced by other
|
|
61
|
+
* tools entirely (generated API documentation, hand-authored Hugo sections), so
|
|
62
|
+
* this build does not hold the set of published pages and would report a working
|
|
63
|
+
* link as dead.
|
|
64
|
+
*
|
|
65
|
+
* @param {ReturnType<typeof buildLinkIndex>} index - The built index.
|
|
66
|
+
* @returns {Array<{note: object, field: string, url: string, text: string,
|
|
67
|
+
* occurrence: number, message: string}>} One finding per defect, `text` and
|
|
68
|
+
* `occurrence` locating it in the note's raw source.
|
|
69
|
+
*/
|
|
70
|
+
export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Array<{
|
|
71
|
+
note: object;
|
|
72
|
+
field: string;
|
|
73
|
+
url: string;
|
|
74
|
+
text: string;
|
|
75
|
+
occurrence: number;
|
|
76
|
+
message: string;
|
|
77
|
+
}>;
|
|
29
78
|
/**
|
|
30
79
|
* Every link in a tree that lands nowhere.
|
|
31
80
|
*
|
|
32
81
|
* @param {ReturnType<typeof buildLinkIndex>} index - The built index.
|
|
33
82
|
* @returns {{deadAnchors: object[], deadAddresses: object[],
|
|
34
|
-
* frontmatterLinks: object[],
|
|
35
|
-
* which addresses a foreign
|
|
83
|
+
* frontmatterLinks: object[], homepageLinks: object[],
|
|
84
|
+
* usedManifest: Set<string>}} The findings, and which addresses a foreign
|
|
85
|
+
* manifest answered.
|
|
36
86
|
*/
|
|
37
87
|
export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
|
|
38
88
|
deadAnchors: object[];
|
|
39
89
|
deadAddresses: object[];
|
|
40
90
|
frontmatterLinks: object[];
|
|
91
|
+
homepageLinks: object[];
|
|
41
92
|
usedManifest: Set<string>;
|
|
42
93
|
};
|
|
43
94
|
/**
|
|
@@ -16,12 +16,15 @@ export function isValidShortcode(value: unknown): boolean;
|
|
|
16
16
|
* @param {object} [opts]
|
|
17
17
|
* @param {readonly string[]} [opts.skipDirectories] - Directory names the walk
|
|
18
18
|
* ignores. Defaults to the configured list.
|
|
19
|
+
* @param {string} [opts.contentPackage] - The package this tree builds, for the
|
|
20
|
+
* homepage rule. Dropped from that finding when unknown rather than guessed.
|
|
19
21
|
* @returns {{findings: Array<{file: string, line?: number, column?: number,
|
|
20
22
|
* severity: "error"|"warning", message: string}>, notes: number,
|
|
21
23
|
* keys: number}} The findings, and what was inspected to produce them.
|
|
22
24
|
*/
|
|
23
|
-
export function lintContentTree(contentBase: string, { skipDirectories }?: {
|
|
25
|
+
export function lintContentTree(contentBase: string, { skipDirectories, contentPackage }?: {
|
|
24
26
|
skipDirectories?: readonly string[] | undefined;
|
|
27
|
+
contentPackage?: string | undefined;
|
|
25
28
|
}): {
|
|
26
29
|
findings: Array<{
|
|
27
30
|
file: string;
|