@heroiclands/package-build 3.4.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -0
- package/CONTENT.md +100 -22
- package/MIGRATING.md +120 -0
- package/bin/content-build.mjs +6 -2
- package/content-config.mjs +109 -6
- package/engine/base-compiler.mjs +33 -36
- package/engine/content-links.mjs +15 -13
- package/engine/content-package.mjs +3 -2
- package/engine/field-reference.mjs +2 -2
- package/engine/frontmatter-lint.mjs +26 -0
- package/engine/helpers.mjs +10 -12
- package/engine/homepage.mjs +150 -0
- package/engine/index.mjs +10 -1
- package/engine/journals.mjs +2 -3
- package/engine/macros.mjs +2 -3
- package/engine/manifest-emit.mjs +17 -12
- package/engine/note-package.mjs +75 -68
- package/engine/note-schemas.mjs +44 -0
- package/engine/pack-router.mjs +3 -3
- package/engine/retired-fields.mjs +123 -0
- package/engine/scenes.mjs +7 -11
- package/engine/site-build.mjs +151 -22
- package/engine/site-index.mjs +5 -5
- package/package.json +1 -1
- package/sohl/actors.mjs +2 -3
- package/sohl/items.mjs +2 -3
- package/sohl/note-schemas.mjs +8 -0
- package/types/content-config.d.mts +105 -4
- package/types/engine/base-compiler.d.mts +12 -17
- package/types/engine/content-package.d.mts +3 -2
- package/types/engine/helpers.d.mts +5 -8
- package/types/engine/homepage.d.mts +118 -0
- package/types/engine/index.d.mts +3 -0
- package/types/engine/manifest-emit.d.mts +7 -8
- package/types/engine/note-package.d.mts +29 -34
- package/types/engine/note-schemas.d.mts +6 -0
- package/types/engine/pack-router.d.mts +3 -3
- package/types/engine/retired-fields.d.mts +54 -0
- package/types/engine/site-build.d.mts +52 -4
- package/types/sohl/note-schemas.d.mts +6 -0
package/engine/site-build.mjs
CHANGED
|
@@ -62,7 +62,14 @@ import {
|
|
|
62
62
|
} from "./foreign-manifests.mjs";
|
|
63
63
|
import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
64
64
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
65
|
-
import {
|
|
65
|
+
import { searchableFrontmatter } from "./note-package.mjs";
|
|
66
|
+
import {
|
|
67
|
+
HOMEPAGE_DESTINATION,
|
|
68
|
+
homepageFrontmatter,
|
|
69
|
+
homepageTitle,
|
|
70
|
+
isHomepage,
|
|
71
|
+
} from "./homepage.mjs";
|
|
72
|
+
import { publishesContentPages } from "../content-config.mjs";
|
|
66
73
|
|
|
67
74
|
const require = createRequire(import.meta.url);
|
|
68
75
|
|
|
@@ -132,12 +139,15 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
132
139
|
const note = readNote(file);
|
|
133
140
|
if (!note) continue;
|
|
134
141
|
const { fm, body } = note;
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// (#56).
|
|
138
|
-
|
|
139
|
-
const pkg = notePackage(fm, ctx.contentPackage);
|
|
142
|
+
// The configuration's, never a note's: `package:` is retired, so every
|
|
143
|
+
// note in the tree belongs to the package this repository compiles
|
|
144
|
+
// (#56).
|
|
145
|
+
const pkg = ctx.contentPackage;
|
|
140
146
|
if (!ctx.packages.has(pkg) || !fm.type) continue;
|
|
147
|
+
// A homepage is addressed by the *package*, not by its own name, so it
|
|
148
|
+
// never takes a section and a slug (#51). {@link collectHomepages}
|
|
149
|
+
// gathers it instead.
|
|
150
|
+
if (isHomepage(fm)) continue;
|
|
141
151
|
|
|
142
152
|
for (const hit of frontmatterWikilinks(fm)) {
|
|
143
153
|
fmLinkFindings.push({ file, ...hit });
|
|
@@ -162,9 +172,9 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
162
172
|
pages.push({
|
|
163
173
|
kind: "content",
|
|
164
174
|
fm,
|
|
165
|
-
// The page's package,
|
|
175
|
+
// The page's package, recorded once here so every consumer — the
|
|
166
176
|
// index's canonical keys, the table universe, the local-package set
|
|
167
|
-
// — reads one
|
|
177
|
+
// — reads one configured value and never frontmatter (#56).
|
|
168
178
|
pkg,
|
|
169
179
|
body,
|
|
170
180
|
name,
|
|
@@ -250,6 +260,62 @@ export function collectTreePages(tree, ctx) {
|
|
|
250
260
|
return { pages, fmLinkFindings };
|
|
251
261
|
}
|
|
252
262
|
|
|
263
|
+
/**
|
|
264
|
+
* The package's homepage notes — the authored page at `/<contentPackage>/`.
|
|
265
|
+
*
|
|
266
|
+
* A separate walk from {@link collectContentPages} rather than a branch inside
|
|
267
|
+
* it, because in homepage-only mode it is the **whole** of the site build: the
|
|
268
|
+
* content tree is never read for pages at all, so the licensing constraint two
|
|
269
|
+
* packages ship under is a property of the code path rather than of a
|
|
270
|
+
* configuration that happens to be empty (#55).
|
|
271
|
+
*
|
|
272
|
+
* Returned as a list rather than as the one note there should be. Requiring
|
|
273
|
+
* exactly one is #52's, and it is a separate decision — this reports what it
|
|
274
|
+
* found so a count is visible either way.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} contentBase - Absolute path to the content tree.
|
|
277
|
+
* @param {object} ctx - `{ skipDirectories }`.
|
|
278
|
+
* @returns {{pages: object[]}} The homepage notes, in walk order.
|
|
279
|
+
*/
|
|
280
|
+
export function collectHomepages(contentBase, ctx) {
|
|
281
|
+
const pages = [];
|
|
282
|
+
for (const file of walkSiteTree(contentBase, ctx.skipDirectories)) {
|
|
283
|
+
const note = readNote(file);
|
|
284
|
+
if (!note || !isHomepage(note.fm)) continue;
|
|
285
|
+
pages.push({ kind: "homepage", file, fm: note.fm, body: note.body });
|
|
286
|
+
}
|
|
287
|
+
return { pages };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Writes each homepage at the package's own root.
|
|
292
|
+
*
|
|
293
|
+
* Its own writer, deliberately small. A homepage is authored markdown published
|
|
294
|
+
* verbatim — no table expansion, no section landing, and (until #54) no link
|
|
295
|
+
* resolution — so routing it through {@link renderPages} would buy it a pipeline
|
|
296
|
+
* it has no input for, and would make homepage-only mode depend on the index,
|
|
297
|
+
* the foreign manifests and the table universe that mode exists to not build.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} outRoot - The package's site root — the configured `site.out`,
|
|
300
|
+
* one level above the content mount.
|
|
301
|
+
* @param {readonly object[]} pages - From {@link collectHomepages}.
|
|
302
|
+
* @param {object} config - The resolved configuration, for the package name and
|
|
303
|
+
* the default title.
|
|
304
|
+
* @returns {number} How many pages were written.
|
|
305
|
+
*/
|
|
306
|
+
export function writeHomepages(outRoot, pages, config) {
|
|
307
|
+
for (const page of pages) {
|
|
308
|
+
const data = homepageFrontmatter(page.fm, {
|
|
309
|
+
contentPackage: config.contentPackage,
|
|
310
|
+
title: homepageTitle(page.fm, config),
|
|
311
|
+
});
|
|
312
|
+
const dest = path.join(outRoot, HOMEPAGE_DESTINATION);
|
|
313
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
314
|
+
fs.writeFileSync(dest, matter.stringify(page.body, data));
|
|
315
|
+
}
|
|
316
|
+
return pages.length;
|
|
317
|
+
}
|
|
318
|
+
|
|
253
319
|
/**
|
|
254
320
|
* The integrity gates a site build runs before it writes anything.
|
|
255
321
|
*
|
|
@@ -319,6 +385,30 @@ export function siteGates(pages, findings, { manifestDir }) {
|
|
|
319
385
|
return out;
|
|
320
386
|
}
|
|
321
387
|
|
|
388
|
+
/**
|
|
389
|
+
* The gate result of a build that ran none of them.
|
|
390
|
+
*
|
|
391
|
+
* Homepage-only publishes one authored page and resolves nothing, so every gate
|
|
392
|
+
* here is about a surface that mode does not have. The shape is returned all the
|
|
393
|
+
* same, because a caller reads the same fields whichever mode ran and a `null`
|
|
394
|
+
* would make each of them a special case.
|
|
395
|
+
*
|
|
396
|
+
* @returns {object} An all-clear gate result.
|
|
397
|
+
*/
|
|
398
|
+
export function emptyGates() {
|
|
399
|
+
return {
|
|
400
|
+
frontmatterLinks: [],
|
|
401
|
+
slugErrors: [],
|
|
402
|
+
collisions: [],
|
|
403
|
+
staleManifests: [],
|
|
404
|
+
unaddressable: [],
|
|
405
|
+
conflicts: [],
|
|
406
|
+
index: null,
|
|
407
|
+
foreign: null,
|
|
408
|
+
manifests: null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
322
412
|
/** Whether any gate produced a finding. */
|
|
323
413
|
export function gatesFailed(gates) {
|
|
324
414
|
return Boolean(
|
|
@@ -347,7 +437,8 @@ export function tableUniverse(pages) {
|
|
|
347
437
|
const pkg = p.pkg;
|
|
348
438
|
if (!byPackage.has(pkg)) byPackage.set(pkg, []);
|
|
349
439
|
byPackage.get(pkg).push({
|
|
350
|
-
// Package present
|
|
440
|
+
// Package present for a `WHERE … package = "…"` clause,
|
|
441
|
+
// synthesised rather than authored — see
|
|
351
442
|
// {@link searchableFrontmatter} (#56).
|
|
352
443
|
fm: searchableFrontmatter(p.fm, pkg),
|
|
353
444
|
path: p.relPath,
|
|
@@ -367,10 +458,10 @@ export function tableUniverse(pages) {
|
|
|
367
458
|
* redirect stub at each name. They are dropped, and this build emits no
|
|
368
459
|
* redirects of its own.
|
|
369
460
|
*
|
|
370
|
-
* A content page carries the package the build **derived
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
461
|
+
* A content page carries the package the build **derived** (#65). No note
|
|
462
|
+
* declares one — `package:` is retired (#56) — so the note's frontmatter alone
|
|
463
|
+
* would publish a page that does not say which package it belongs to. The
|
|
464
|
+
* emitted page is what a
|
|
374
465
|
* theme reads: `breadcrumbs.html` builds its middle crumb from
|
|
375
466
|
* `.Params.package`, so without it that crumb degrades from a linked, labelled
|
|
376
467
|
* section to a bare type slug. Writing the derived value keeps a page
|
|
@@ -387,10 +478,8 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
|
|
|
387
478
|
if (page.kind === "content") {
|
|
388
479
|
data = {
|
|
389
480
|
...fm,
|
|
390
|
-
// Spread after the note's own frontmatter
|
|
391
|
-
//
|
|
392
|
-
// tree emits byte-identically. Guarded because `package: undefined`
|
|
393
|
-
// is not a value YAML can carry.
|
|
481
|
+
// Spread after the note's own frontmatter. Guarded because
|
|
482
|
+
// `package: undefined` is not a value YAML can carry.
|
|
394
483
|
...(page.pkg ? { package: page.pkg } : {}),
|
|
395
484
|
slug,
|
|
396
485
|
title: fm.title ?? name,
|
|
@@ -708,6 +797,10 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
708
797
|
const resolved = config ?? loadPackConfig();
|
|
709
798
|
const site = resolved.site;
|
|
710
799
|
const scheme = resolved.publish.address;
|
|
800
|
+
// Homepage-only or homepage-plus-content (#55). The floor is the homepage,
|
|
801
|
+
// so this decides whether the *content* surfaces are published, never
|
|
802
|
+
// whether anything is.
|
|
803
|
+
const publishesContent = publishesContentPages(resolved);
|
|
711
804
|
|
|
712
805
|
// Where the package is served, and where its content mounts inside it. The
|
|
713
806
|
// two are separate facts: `base` is the package's own address on the site
|
|
@@ -724,9 +817,16 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
724
817
|
// directory it was launched from (#1508).
|
|
725
818
|
const outBase = resolveOutputRoot(resolved.rootDir, site.out);
|
|
726
819
|
const out =
|
|
727
|
-
outRoot ?
|
|
728
|
-
|
|
729
|
-
|
|
820
|
+
outRoot ? path.resolve(outRoot)
|
|
821
|
+
: publishesContent ?
|
|
822
|
+
path.join(outBase, scheme.prefix.replace(/\/$/, ""))
|
|
823
|
+
// Homepage-only has no content mount, so the package's root *is*
|
|
824
|
+
// the output root and `--out` redirects the whole of it.
|
|
825
|
+
: outBase;
|
|
826
|
+
// The homepage publishes at `/<contentPackage>/`, which is the package's
|
|
827
|
+
// own root — one level above the content mount, and the same directory in
|
|
828
|
+
// homepage-only mode.
|
|
829
|
+
const homeRoot = publishesContent ? outBase : out;
|
|
730
830
|
|
|
731
831
|
const packages = new Set(
|
|
732
832
|
site.packages.length ? site.packages : [resolved.contentPackage],
|
|
@@ -734,7 +834,8 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
734
834
|
|
|
735
835
|
const ctx = {
|
|
736
836
|
packages,
|
|
737
|
-
//
|
|
837
|
+
// The package every note in the tree belongs to. `package:` is
|
|
838
|
+
// retired, so this is the only source of it (#56).
|
|
738
839
|
contentPackage: resolved.contentPackage,
|
|
739
840
|
skipDirectories: resolved.skipDirectories,
|
|
740
841
|
mount,
|
|
@@ -745,6 +846,25 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
745
846
|
// note was deleted or renamed would otherwise linger and keep publishing.
|
|
746
847
|
fs.rmSync(outBase, { recursive: true, force: true });
|
|
747
848
|
|
|
849
|
+
const homepages = collectHomepages(resolved.paths.content, ctx).pages;
|
|
850
|
+
|
|
851
|
+
// Homepage-only stops here, and stopping is the point: nothing below reads
|
|
852
|
+
// the content tree for pages, so `sohl-kethira-basic` and `harn-adventures`
|
|
853
|
+
// cannot publish one whatever else their `site:` block declares (#55).
|
|
854
|
+
if (!publishesContent) {
|
|
855
|
+
return {
|
|
856
|
+
gates: emptyGates(),
|
|
857
|
+
manifests: null,
|
|
858
|
+
tableErrors: [],
|
|
859
|
+
wikiErrors: [],
|
|
860
|
+
stats: {
|
|
861
|
+
homepages: writeHomepages(homeRoot, homepages, resolved),
|
|
862
|
+
landings: 0,
|
|
863
|
+
out: homeRoot,
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
|
|
748
868
|
const content = collectContentPages(resolved.paths.content, ctx);
|
|
749
869
|
const pages = [...content.pages];
|
|
750
870
|
const fmLinkFindings = [...content.fmLinkFindings];
|
|
@@ -805,12 +925,21 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
805
925
|
sectionTitle: site.backfillSections ? pluralTitle : null,
|
|
806
926
|
});
|
|
807
927
|
|
|
928
|
+
// Last, and outside the mount: the package's front page is not part of the
|
|
929
|
+
// content tree it introduces.
|
|
930
|
+
const homepagesWritten = writeHomepages(homeRoot, homepages, resolved);
|
|
931
|
+
|
|
808
932
|
return {
|
|
809
933
|
gates,
|
|
810
934
|
manifests: gates.manifests,
|
|
811
935
|
tableErrors: rendered.tableErrors,
|
|
812
936
|
wikiErrors: rendered.wikiErrors,
|
|
813
|
-
stats: {
|
|
937
|
+
stats: {
|
|
938
|
+
...rendered.byKind,
|
|
939
|
+
homepages: homepagesWritten,
|
|
940
|
+
landings,
|
|
941
|
+
out,
|
|
942
|
+
},
|
|
814
943
|
};
|
|
815
944
|
}
|
|
816
945
|
|
package/engine/site-index.mjs
CHANGED
|
@@ -53,7 +53,7 @@ import path from "node:path";
|
|
|
53
53
|
|
|
54
54
|
import { canonicalKey, readCanonicalKey } from "./kb-manifest.mjs";
|
|
55
55
|
import { hasDocEntry } from "./item-docs.mjs";
|
|
56
|
-
import {
|
|
56
|
+
import { contentPackage } from "./content-package.mjs";
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* One page the site will publish, as the index needs to see it.
|
|
@@ -239,11 +239,11 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
|
|
|
239
239
|
// stays because a bare `[[skill-lang]]` defaults to the citing
|
|
240
240
|
// note's own package and must keep resolving unchanged; the
|
|
241
241
|
// canonical form is what cross-package links use (#1499).
|
|
242
|
-
// The page's package is
|
|
243
|
-
// and records it as `pkg
|
|
244
|
-
//
|
|
242
|
+
// The page's package is the configured one — the site collection
|
|
243
|
+
// resolves it and records it as `pkg`. Never read out of
|
|
244
|
+
// frontmatter: `package:` is retired (#56).
|
|
245
245
|
index.set(
|
|
246
|
-
canonicalKey(e.pkg ??
|
|
246
|
+
canonicalKey(e.pkg ?? contentPackage(), type, shortcode),
|
|
247
247
|
value,
|
|
248
248
|
);
|
|
249
249
|
// In Foundry an item and its documentation are two documents, so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
package/sohl/actors.mjs
CHANGED
|
@@ -34,9 +34,8 @@
|
|
|
34
34
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`). Must run
|
|
35
35
|
* after the items pass, since it reads the items pack's generated JSON tree.
|
|
36
36
|
*
|
|
37
|
-
* The walk itself — filtering by
|
|
38
|
-
*
|
|
39
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
37
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
38
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
40
39
|
* states only what makes this pass its own (#1509).
|
|
41
40
|
*/
|
|
42
41
|
|
package/sohl/items.mjs
CHANGED
|
@@ -29,9 +29,8 @@
|
|
|
29
29
|
* Not a standalone script — exports the `Items` compiler class, imported and
|
|
30
30
|
* driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
|
|
31
31
|
*
|
|
32
|
-
* The walk itself — filtering by
|
|
33
|
-
*
|
|
34
|
-
* errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
32
|
+
* The walk itself — filtering by type, expanding tables, converting
|
|
33
|
+
* wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
|
|
35
34
|
* states only what makes this pass its own (#1509).
|
|
36
35
|
*/
|
|
37
36
|
|
package/sohl/note-schemas.mjs
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { AS_AUTHORED, NUMBER, STRING } from "../engine/field-spec.mjs";
|
|
41
|
+
import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
|
|
41
42
|
import { ITEM_FIELDS } from "./item-fields.mjs";
|
|
42
43
|
|
|
43
44
|
/** A map-valued property, whose entries the compiler walks by key. */
|
|
@@ -302,9 +303,16 @@ const PRESENTATION_FIELDS = Object.freeze({
|
|
|
302
303
|
* Every content type this package compiles, and what a note of that type may
|
|
303
304
|
* write.
|
|
304
305
|
*
|
|
306
|
+
* The engine's own types are merged in first, so a SoHL tree is checked against
|
|
307
|
+
* one vocabulary rather than two. They are declared there rather than here
|
|
308
|
+
* because they are note-format knowledge — a `homepage` carries no `system`
|
|
309
|
+
* block and would mean the same thing for a game system that is not SoHL — and
|
|
310
|
+
* because a package declaring no `itemBuilders` never reaches this file (#51).
|
|
311
|
+
*
|
|
305
312
|
* @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
|
|
306
313
|
*/
|
|
307
314
|
export const NOTE_SCHEMAS = Object.freeze({
|
|
315
|
+
...ENGINE_NOTE_SCHEMAS,
|
|
308
316
|
...Object.fromEntries(
|
|
309
317
|
Object.entries(ITEM_FIELDS).map(([type, fields]) => [
|
|
310
318
|
type,
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this package publishes the pages its content tree compiles to.
|
|
3
|
+
*
|
|
4
|
+
* The one question every reader of the mode actually asks — the site build, to
|
|
5
|
+
* decide whether to walk the tree at all, and the link-manifest emitter, to
|
|
6
|
+
* decide whether an entry carries a web `path`. Written once here so the two
|
|
7
|
+
* cannot come to disagree about what a mode means.
|
|
8
|
+
*
|
|
9
|
+
* @param {{publish: {site: SiteMode}}} config - A resolved configuration.
|
|
10
|
+
* @returns {boolean} Whether content pages are published.
|
|
11
|
+
*/
|
|
12
|
+
export function publishesContentPages(config: {
|
|
13
|
+
publish: {
|
|
14
|
+
site: SiteMode;
|
|
15
|
+
};
|
|
16
|
+
}): boolean;
|
|
1
17
|
/**
|
|
2
18
|
* Validate and normalize a content configuration.
|
|
3
19
|
*
|
|
@@ -76,6 +92,77 @@ export const DEFAULT_ADDRESS_SCHEME: Readonly<{
|
|
|
76
92
|
prefix: "";
|
|
77
93
|
landing: "readme";
|
|
78
94
|
}>;
|
|
95
|
+
/**
|
|
96
|
+
* How much of a package reaches the web.
|
|
97
|
+
*
|
|
98
|
+
* Every HeroicLands package publishes something: a top-level, human-authored
|
|
99
|
+
* homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
|
|
100
|
+
* module is, which system it needs and how to install it (#50). So there is no
|
|
101
|
+
* value here meaning *no web presence at all* — homepage-only is the **floor**,
|
|
102
|
+
* and the default.
|
|
103
|
+
*
|
|
104
|
+
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
105
|
+
* is not walked for pages, `site.sections` / `site.trees` / `site.landing`
|
|
106
|
+
* emit nothing, and link-manifest entries carry no web `path`.
|
|
107
|
+
* - `content` — the homepage *plus* every page the content tree publishes: the
|
|
108
|
+
* knowledgebase, the extra trees, the section landings.
|
|
109
|
+
*
|
|
110
|
+
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
111
|
+
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
112
|
+
* Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
|
|
113
|
+
* terms) must each publish a homepage and nothing beneath it — two packages
|
|
114
|
+
* under two different fan-content licences. The boundary is **published
|
|
115
|
+
* content**: journal text, artwork, item descriptions, compiled notes. A
|
|
116
|
+
* human-authored page announcing the module discloses none of it. Because the
|
|
117
|
+
* failure mode is silent — a `site:` block added later ships licensed content
|
|
118
|
+
* with nobody noticing — the mode fences the content surfaces off rather than
|
|
119
|
+
* trusting a configuration to stay empty.
|
|
120
|
+
*
|
|
121
|
+
* This was a boolean until 5.0.0, and `false` read as "no web presence", which
|
|
122
|
+
* no longer describes any package. Both spellings are refused rather than
|
|
123
|
+
* mapped: a value silently reinterpreted reads to its author as though it still
|
|
124
|
+
* means what it said.
|
|
125
|
+
*
|
|
126
|
+
* @typedef {"homepage" | "content"} SiteMode
|
|
127
|
+
*/
|
|
128
|
+
/**
|
|
129
|
+
* The publishing modes {@link PublishSwitches.site} may name, floor first.
|
|
130
|
+
*
|
|
131
|
+
* @satisfies {readonly SiteMode[]}
|
|
132
|
+
*/
|
|
133
|
+
export const SITE_MODES: readonly ["homepage", "content"];
|
|
134
|
+
/**
|
|
135
|
+
* How much of a package reaches the web.
|
|
136
|
+
*
|
|
137
|
+
* Every HeroicLands package publishes something: a top-level, human-authored
|
|
138
|
+
* homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
|
|
139
|
+
* module is, which system it needs and how to install it (#50). So there is no
|
|
140
|
+
* value here meaning *no web presence at all* — homepage-only is the **floor**,
|
|
141
|
+
* and the default.
|
|
142
|
+
*
|
|
143
|
+
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
144
|
+
* is not walked for pages, `site.sections` / `site.trees` / `site.landing`
|
|
145
|
+
* emit nothing, and link-manifest entries carry no web `path`.
|
|
146
|
+
* - `content` — the homepage *plus* every page the content tree publishes: the
|
|
147
|
+
* knowledgebase, the extra trees, the section landings.
|
|
148
|
+
*
|
|
149
|
+
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
150
|
+
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
151
|
+
* Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
|
|
152
|
+
* terms) must each publish a homepage and nothing beneath it — two packages
|
|
153
|
+
* under two different fan-content licences. The boundary is **published
|
|
154
|
+
* content**: journal text, artwork, item descriptions, compiled notes. A
|
|
155
|
+
* human-authored page announcing the module discloses none of it. Because the
|
|
156
|
+
* failure mode is silent — a `site:` block added later ships licensed content
|
|
157
|
+
* with nobody noticing — the mode fences the content surfaces off rather than
|
|
158
|
+
* trusting a configuration to stay empty.
|
|
159
|
+
*
|
|
160
|
+
* This was a boolean until 5.0.0, and `false` read as "no web presence", which
|
|
161
|
+
* no longer describes any package. Both spellings are refused rather than
|
|
162
|
+
* mapped: a value silently reinterpreted reads to its author as though it still
|
|
163
|
+
* means what it said.
|
|
164
|
+
*/
|
|
165
|
+
export type SiteMode = "homepage" | "content";
|
|
79
166
|
export type PackageKind = "systems" | "modules";
|
|
80
167
|
export type PackDocumentType = "Actor" | "Adventure" | "Item" | "JournalEntry" | "Macro" | "Scene";
|
|
81
168
|
/**
|
|
@@ -272,9 +359,10 @@ export type ManifestSwitches = {
|
|
|
272
359
|
};
|
|
273
360
|
export type PublishSwitches = {
|
|
274
361
|
/**
|
|
275
|
-
*
|
|
362
|
+
* How much of this package reaches the web.
|
|
363
|
+
* See {@link SITE_MODES}.
|
|
276
364
|
*/
|
|
277
|
-
site:
|
|
365
|
+
site: SiteMode;
|
|
278
366
|
manifests: ManifestSwitches;
|
|
279
367
|
};
|
|
280
368
|
export type ManifestSwitchesInput = {
|
|
@@ -394,8 +482,19 @@ export type DocsSpec = {
|
|
|
394
482
|
itemFields?: DocPageSpec | undefined;
|
|
395
483
|
};
|
|
396
484
|
export type PublishSwitchesInput = {
|
|
397
|
-
site?:
|
|
485
|
+
site?: SiteMode | undefined;
|
|
398
486
|
manifests?: ManifestSwitchesInput | undefined;
|
|
487
|
+
address?: AddressSchemeInput | undefined;
|
|
488
|
+
};
|
|
489
|
+
export type AddressSchemeInput = {
|
|
490
|
+
/**
|
|
491
|
+
* Where the content tree mounts inside the package.
|
|
492
|
+
*/
|
|
493
|
+
prefix?: string | undefined;
|
|
494
|
+
/**
|
|
495
|
+
* Which note addresses a whole section.
|
|
496
|
+
*/
|
|
497
|
+
landing?: string | undefined;
|
|
399
498
|
};
|
|
400
499
|
/**
|
|
401
500
|
* One entry of a consumer's `itemBuilders` registry.
|
|
@@ -502,7 +601,9 @@ export type ContentBuildConfigInput = {
|
|
|
502
601
|
*/
|
|
503
602
|
relationships?: Relationships | undefined;
|
|
504
603
|
/**
|
|
505
|
-
* Publishing switches.
|
|
604
|
+
* Publishing switches. The manifest
|
|
605
|
+
* switches default to off; `site`
|
|
606
|
+
* defaults to `homepage`, the floor.
|
|
506
607
|
*/
|
|
507
608
|
publish?: PublishSwitchesInput | undefined;
|
|
508
609
|
};
|
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
* The tallies one pass accumulates while walking the tree.
|
|
3
3
|
*
|
|
4
4
|
* `declined` and `skippedOther` are deliberately separate numbers. A declined
|
|
5
|
-
* note is one this build **refused** — it
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* note is one this build **refused** — it declares a retired frontmatter field
|
|
6
|
+
* — and it is an error; a skipped one legitimately belongs to another pass, and
|
|
7
|
+
* there are thousands of those. Folding the first into the second is what let a
|
|
8
|
+
* whole tree be filtered out in silence (#56).
|
|
9
9
|
*
|
|
10
10
|
* @typedef {object} PassStats
|
|
11
11
|
* @property {number} compiled - Notes that became a document.
|
|
12
|
-
* @property {number} skippedDraft - Notes marked `draft: true`.
|
|
13
12
|
* @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
|
|
14
13
|
* @property {number} skippedOther - Notes this pass does not claim.
|
|
15
|
-
* @property {number} declined - Notes refused because they declare
|
|
16
|
-
*
|
|
14
|
+
* @property {number} declined - Notes refused because they declare a retired
|
|
15
|
+
* frontmatter field. Counted as errors, never as skips.
|
|
17
16
|
*/
|
|
18
17
|
/**
|
|
19
18
|
* The shared walk → filter → expand → convert → build → write → count loop.
|
|
@@ -310,20 +309,16 @@ export class BasePackCompiler {
|
|
|
310
309
|
* The tallies one pass accumulates while walking the tree.
|
|
311
310
|
*
|
|
312
311
|
* `declined` and `skippedOther` are deliberately separate numbers. A declined
|
|
313
|
-
* note is one this build **refused** — it
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
312
|
+
* note is one this build **refused** — it declares a retired frontmatter field
|
|
313
|
+
* — and it is an error; a skipped one legitimately belongs to another pass, and
|
|
314
|
+
* there are thousands of those. Folding the first into the second is what let a
|
|
315
|
+
* whole tree be filtered out in silence (#56).
|
|
317
316
|
*/
|
|
318
317
|
export type PassStats = {
|
|
319
318
|
/**
|
|
320
319
|
* - Notes that became a document.
|
|
321
320
|
*/
|
|
322
321
|
compiled: number;
|
|
323
|
-
/**
|
|
324
|
-
* - Notes marked `draft: true`.
|
|
325
|
-
*/
|
|
326
|
-
skippedDraft: number;
|
|
327
322
|
/**
|
|
328
323
|
* - Notes with no `id`, where that is tolerated.
|
|
329
324
|
*/
|
|
@@ -333,8 +328,8 @@ export type PassStats = {
|
|
|
333
328
|
*/
|
|
334
329
|
skippedOther: number;
|
|
335
330
|
/**
|
|
336
|
-
* - Notes refused because they declare
|
|
337
|
-
*
|
|
331
|
+
* - Notes refused because they declare a retired
|
|
332
|
+
* frontmatter field. Counted as errors, never as skips.
|
|
338
333
|
*/
|
|
339
334
|
declined: number;
|
|
340
335
|
};
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* `package:` frontmatter and the compilers kept the ones that matched. Every
|
|
13
13
|
* content tree is single-package — each is single-sourced in the repository that
|
|
14
14
|
* ships it — so the field restated this constant once per note while a value
|
|
15
|
-
* that matched nothing filtered the whole tree out in silence.
|
|
16
|
-
*
|
|
15
|
+
* that matched nothing filtered the whole tree out in silence. That field is
|
|
16
|
+
* retired and declaring it now fails the build; this value stays, here, where
|
|
17
|
+
* it is declared once.
|
|
17
18
|
*
|
|
18
19
|
* Stable across compilation targets. If this content were ever compiled for a
|
|
19
20
|
* second game system, it would still be published as `sohl` — only the Foundry
|
|
@@ -274,17 +274,15 @@ export function collectContentDocs(contentBase: string): Array<{
|
|
|
274
274
|
* Expand the fenced `dataview` tables in one note's markdown, before wikilinks
|
|
275
275
|
* are resolved — so a generated cell may itself be a wikilink.
|
|
276
276
|
*
|
|
277
|
-
* A table searches
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
* unswept — or every swept — note from the table (#56).
|
|
277
|
+
* A table searches the whole tree, which is one package's notes and nothing
|
|
278
|
+
* else — so there is no longer a package to scope on. It used to filter, back
|
|
279
|
+
* when a tree could hold several packages' notes and `package:` said which was
|
|
280
|
+
* which; that field is retired and the filter with it (#56).
|
|
282
281
|
*
|
|
283
282
|
* @param {string} body - The note's markdown body.
|
|
284
283
|
* @param {object} ctx
|
|
285
284
|
* @param {Array<object>} ctx.docs - From {@link collectContentDocs}.
|
|
286
285
|
* @param {string} ctx.name - The note, for the error message.
|
|
287
|
-
* @param {string} [ctx.pkg] - The source note's package.
|
|
288
286
|
* @param {object} [ctx.fm] - The source note's frontmatter, which is what a
|
|
289
287
|
* query's `this` reads. Its entry in `docs` supplies the path as well.
|
|
290
288
|
* @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
|
|
@@ -297,10 +295,9 @@ export function collectContentDocs(contentBase: string): Array<{
|
|
|
297
295
|
* compile rather than shipping a table-shaped hole. The error carries
|
|
298
296
|
* `position`, the directive's own line.
|
|
299
297
|
*/
|
|
300
|
-
export function expandNoteTables(body: string, { docs, name,
|
|
298
|
+
export function expandNoteTables(body: string, { docs, name, fm, bodyLine }: {
|
|
301
299
|
docs: Array<object>;
|
|
302
300
|
name: string;
|
|
303
|
-
pkg?: string | undefined;
|
|
304
301
|
fm?: object | undefined;
|
|
305
302
|
bodyLine?: number | undefined;
|
|
306
303
|
}): {
|