@heroiclands/package-build 4.0.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 +82 -0
- package/CONTENT.md +71 -10
- package/MIGRATING.md +73 -0
- package/bin/content-build.mjs +6 -2
- package/content-config.mjs +109 -6
- package/engine/homepage.mjs +150 -0
- package/engine/index.mjs +6 -0
- package/engine/manifest-emit.mjs +2 -1
- package/engine/note-schemas.mjs +44 -0
- package/engine/site-build.mjs +134 -4
- package/package.json +1 -1
- package/sohl/note-schemas.mjs +8 -0
- package/types/content-config.d.mts +105 -4
- package/types/engine/homepage.d.mts +118 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/note-schemas.d.mts +6 -0
- package/types/engine/site-build.d.mts +48 -0
- package/types/sohl/note-schemas.d.mts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 4da0dbc: Every package publishes an authored homepage at `/<contentPackage>/`, and
|
|
8
|
+
`publish.site` becomes a mode rather than a boolean (#51, #55).
|
|
9
|
+
|
|
10
|
+
**A `type: homepage` note (#51)**
|
|
11
|
+
|
|
12
|
+
A new engine-level content type that compiles into a **page** rather than into a
|
|
13
|
+
compendium document. Its whole frontmatter envelope is `type` and an optional
|
|
14
|
+
`title`, defaulting to `packageBuild.manifest.title` so the package's name is not
|
|
15
|
+
written twice:
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
---
|
|
19
|
+
type: homepage
|
|
20
|
+
title: HârnMaster Kethira Basic
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
What the module is, which system it needs, how to install it.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It compiles into no document, appears in no pack and in no link manifest, and is
|
|
27
|
+
addressed by the **package** — `/<contentPackage>/` — rather than by a slug
|
|
28
|
+
derived from its name, so `name.full`, `shortcode` and `id` decide nothing on it.
|
|
29
|
+
It is written at the root of `site.out`, one level above the content mount.
|
|
30
|
+
|
|
31
|
+
The page is _authored, not assembled_. Deriving it from the manifest, the release
|
|
32
|
+
address and `relationships` was the obvious shortcut and produces a page nobody
|
|
33
|
+
chose the contents of — it cannot express that Kethira requires buying the book
|
|
34
|
+
from Keléstia, or which of twenty sections a reader should start with.
|
|
35
|
+
|
|
36
|
+
It is declared in `engine/note-schemas.mjs` rather than in the `sohl` item
|
|
37
|
+
registry: the `engine/` ÷ `sohl/` line is note-format knowledge against
|
|
38
|
+
game-system knowledge, and a homepage carries no `system` block and mirrors no
|
|
39
|
+
item builder. Reachability is the symptom that makes it obvious —
|
|
40
|
+
`HarnMaster-3-FoundryVTT` declares no `itemBuilders` at all, so a type living in
|
|
41
|
+
the SoHL registry would be unavailable to HM3 and every HM3 module, which is most
|
|
42
|
+
of the packages that need a homepage and nothing else.
|
|
43
|
+
|
|
44
|
+
**`publish.site` is a mode (#55) — breaking**
|
|
45
|
+
|
|
46
|
+
| Was | Write |
|
|
47
|
+
| ------------- | ---------------------------------- |
|
|
48
|
+
| `site: true` | `site: content` |
|
|
49
|
+
| `site: false` | `site: homepage` |
|
|
50
|
+
| absent | absent — the default is `homepage` |
|
|
51
|
+
|
|
52
|
+
`homepage` publishes the authored homepage and **no other page**; `content`
|
|
53
|
+
publishes it plus every page the content tree compiles to. There is no value
|
|
54
|
+
meaning "no web presence", because every package publishes its homepage.
|
|
55
|
+
|
|
56
|
+
Both booleans are **refused rather than mapped** onto the nearest mode, naming
|
|
57
|
+
the mode to write. `false` read as _this package has no web presence_, which
|
|
58
|
+
describes no package now, and a value silently reinterpreted reads to its author
|
|
59
|
+
as though it still means what it said.
|
|
60
|
+
|
|
61
|
+
**Homepage-only is a first-class mode, not an accommodation.**
|
|
62
|
+
`sohl-kethira-basic` (Keléstia Productions' Fan Material Guidelines) and
|
|
63
|
+
`harn-adventures` (HârnFanon under Lythia's terms) each publish a homepage and
|
|
64
|
+
nothing beneath it — two packages under two different fan-content licences. The
|
|
65
|
+
boundary is _published content_: journal text, artwork, item descriptions,
|
|
66
|
+
compiled notes. Because the failure mode is silent — a `site:` block added later
|
|
67
|
+
ships licensed content with nobody noticing — the mode **fences the content
|
|
68
|
+
surfaces off**: in `homepage` mode the tree is never walked for pages, and
|
|
69
|
+
`sections`, `trees`, `landing` and `backfillSections` emit nothing even when they
|
|
70
|
+
are declared. Measured against the real `sohl-kethira-basic` tree — 363 notes,
|
|
71
|
+
and a `site:` block deliberately declaring sections and a landing — the build
|
|
72
|
+
emits exactly one file.
|
|
73
|
+
|
|
74
|
+
`publish.manifests.publish` is a separate decision and stays `false` for both, for
|
|
75
|
+
an unrelated reason: a link manifest is the dependency edge that would stop a
|
|
76
|
+
module being withdrawable, and a homepage is one row in a routing table.
|
|
77
|
+
|
|
78
|
+
**Nothing else moves.** `publish.address`, `publish.manifests` and the whole
|
|
79
|
+
`site:` block are unchanged, and every address `sohl` already publishes is
|
|
80
|
+
byte-identical across the upgrade. Verified against the real tree: 1,669 emitted
|
|
81
|
+
files before, 1,670 after, the one addition being `kb/content/_index.md`; the
|
|
82
|
+
link manifest's 2,989 entries and all 3,126 compiled pack documents are
|
|
83
|
+
byte-identical with and without the homepage note.
|
|
84
|
+
|
|
3
85
|
## 4.0.0
|
|
4
86
|
|
|
5
87
|
### Major Changes
|
package/CONTENT.md
CHANGED
|
@@ -128,9 +128,11 @@ packageBuild:
|
|
|
128
128
|
- { from: assets/icons, to: assets/icons }
|
|
129
129
|
|
|
130
130
|
# Three independent switches — every combination is real — plus the address
|
|
131
|
-
# scheme both `manifest` and `site` derive addresses under.
|
|
131
|
+
# scheme both `manifest` and `site` derive addresses under. `site` is a mode,
|
|
132
|
+
# not a boolean: `homepage` (the default) publishes the authored homepage and
|
|
133
|
+
# no other page; `content` publishes it plus every page the tree compiles to.
|
|
132
134
|
publish:
|
|
133
|
-
site:
|
|
135
|
+
site: content
|
|
134
136
|
manifests: { publish: true, consume: true }
|
|
135
137
|
address:
|
|
136
138
|
prefix: kb/
|
|
@@ -147,8 +149,8 @@ site:
|
|
|
147
149
|
|
|
148
150
|
The loader validates the document, resolves every path against the directory
|
|
149
151
|
the file sits in, fills the optional halves with their defaults
|
|
150
|
-
(`skipDirectories: []`, `packageBuild: {}`, the conventional `paths`,
|
|
151
|
-
|
|
152
|
+
(`skipDirectories: []`, `packageBuild: {}`, the conventional `paths`, both
|
|
153
|
+
manifest switches off and `publish.site` at its `homepage` floor),
|
|
152
154
|
derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
|
|
153
155
|
freezes the result. A malformed configuration throws a `TypeError` naming the
|
|
154
156
|
offending field, so it fails at load rather than as an empty pack much later.
|
|
@@ -594,10 +596,11 @@ It reads its whole input from configuration and takes nothing else:
|
|
|
594
596
|
| `publish.address` | The address scheme those paths are derived under — see below. |
|
|
595
597
|
|
|
596
598
|
**Both addresses are optional, independently.** A note that compiles into no
|
|
597
|
-
document has no `uuid`, and a package that ships compendiums and publishes
|
|
598
|
-
|
|
599
|
-
neither is guessed: inventing the missing one
|
|
600
|
-
exist, which is the silent dead link the manifest
|
|
599
|
+
document has no `uuid`, and a package that ships compendiums and publishes only
|
|
600
|
+
a homepage (`publish.site: homepage`) has no `path` on any entry — its notes are
|
|
601
|
+
not pages. Neither is an error, and neither is guessed: inventing the missing one
|
|
602
|
+
asserts a target that does not exist, which is the silent dead link the manifest
|
|
603
|
+
exists to prevent.
|
|
601
604
|
|
|
602
605
|
**`publish.manifests.publish` is a declaration, not a preference.** The file is
|
|
603
606
|
vendored by other repositories and read as authoritative, so emitting one is a
|
|
@@ -615,7 +618,7 @@ time and 404s for the reader.
|
|
|
615
618
|
|
|
616
619
|
```yaml
|
|
617
620
|
publish:
|
|
618
|
-
site:
|
|
621
|
+
site: content
|
|
619
622
|
manifests: { publish: true, consume: true }
|
|
620
623
|
address:
|
|
621
624
|
prefix: kb/ # default: "" — the package root
|
|
@@ -658,6 +661,64 @@ address derivation, the address index, table expansion, wikilink resolution,
|
|
|
658
661
|
code-fence protection, the foreign-manifest merge, the page emission and the
|
|
659
662
|
section-landing backfill.
|
|
660
663
|
|
|
664
|
+
### The homepage, and how much else is published
|
|
665
|
+
|
|
666
|
+
Every package is reachable at `https://www.heroiclands.org/<contentPackage>/`,
|
|
667
|
+
and what a reader finds there is a note in the content tree — one markdown file,
|
|
668
|
+
written by a person:
|
|
669
|
+
|
|
670
|
+
```markdown
|
|
671
|
+
---
|
|
672
|
+
type: homepage
|
|
673
|
+
title: HârnMaster Kethira Basic # optional; defaults to packageBuild.manifest.title
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
What the module is, which system it needs, how to install it.
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
That is the whole envelope. A homepage **compiles into no compendium
|
|
680
|
+
document**, appears in no pack and in no link manifest, and is addressed by the
|
|
681
|
+
_package_ rather than by its own name — so `name.full`, `shortcode` and `id`
|
|
682
|
+
decide nothing on it. It is dispatched on `type` like every other note, not on a
|
|
683
|
+
filename: `README.md` is already a section landing under `landing: readme`, and
|
|
684
|
+
in `sohl-thalorna` it is a developer explainer about the source tree.
|
|
685
|
+
|
|
686
|
+
`type: homepage` is declared by the **engine**, not by the `sohl` item registry,
|
|
687
|
+
so a package that configures no `itemBuilders` at all — `HarnMaster-3-FoundryVTT`
|
|
688
|
+
and every HM3 module — can author one. The `engine/` ÷ `sohl/` line is
|
|
689
|
+
note-format knowledge against game-system knowledge, and a homepage carries no
|
|
690
|
+
`system` block.
|
|
691
|
+
|
|
692
|
+
`publish.site` then says how much _else_ is published:
|
|
693
|
+
|
|
694
|
+
| Mode | What is published |
|
|
695
|
+
| ---------- | ------------------------------------------------------------------------------- |
|
|
696
|
+
| `homepage` | The authored homepage, and no other page. **The default, and the floor.** |
|
|
697
|
+
| `content` | The homepage plus every page the content tree compiles to, and its extra trees. |
|
|
698
|
+
|
|
699
|
+
There is no value meaning "no web presence": every package publishes its
|
|
700
|
+
homepage. It was a boolean until 5.0.0, and both spellings are now refused
|
|
701
|
+
naming the mode to write instead — see [MIGRATING.md](MIGRATING.md).
|
|
702
|
+
|
|
703
|
+
**Homepage-only is a first-class mode, not an accommodation.**
|
|
704
|
+
`sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
705
|
+
Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's terms)
|
|
706
|
+
must each publish a homepage and nothing beneath it. The boundary is _published
|
|
707
|
+
content_ — journal text, artwork, item descriptions, compiled notes — and a page
|
|
708
|
+
announcing the module discloses none of it. Because the failure mode is silent,
|
|
709
|
+
the mode **fences the content surfaces off** rather than trusting a
|
|
710
|
+
configuration to stay empty: in `homepage` mode the tree is never walked for
|
|
711
|
+
pages, and `sections`, `trees`, `landing` and `backfillSections` emit nothing
|
|
712
|
+
even when they are declared.
|
|
713
|
+
|
|
714
|
+
That is separate from `publish.manifests.publish`, which stays off for both for
|
|
715
|
+
an unrelated reason: a link manifest is the dependency edge that would stop the
|
|
716
|
+
module being withdrawable, and a homepage is one row in a routing table.
|
|
717
|
+
|
|
718
|
+
The homepage is written at the root of `site.out` — the package's own address —
|
|
719
|
+
one level above the content mount, which is where `publish.address.prefix` puts
|
|
720
|
+
everything else.
|
|
721
|
+
|
|
661
722
|
**What it does not do is decide addresses.** Those come from `publish.address`,
|
|
662
723
|
the same setting the link manifest reads, so a page and its manifest entry cannot
|
|
663
724
|
disagree about where the page is. Everything under `site:` is _framing_ —
|
|
@@ -686,7 +747,7 @@ site:
|
|
|
686
747
|
|
|
687
748
|
| Key | What it decides |
|
|
688
749
|
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
|
689
|
-
| `out` | The Hugo content root. **Required
|
|
750
|
+
| `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below. |
|
|
690
751
|
| `base` | Where the package is served. Defaults to `/<contentPackage>/`. |
|
|
691
752
|
| `packages` | Which content packages this site renders. Defaults to its own. |
|
|
692
753
|
| `sections` | Landing title and hero per section, so a landing matches the card that links to it. |
|
package/MIGRATING.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
+
# Migrating to `@heroiclands/package-build` 5.0.0
|
|
2
|
+
|
|
3
|
+
**One configuration change: `publish.site` is a mode, not a boolean.** And one
|
|
4
|
+
new authoring capability that needs no migration: a `type: homepage` note.
|
|
5
|
+
|
|
6
|
+
## 1. Respell `publish.site`
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
publish:
|
|
10
|
+
site: content # was `site: true`
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
publish:
|
|
15
|
+
site: homepage # was `site: false`, or absent
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`homepage` is the default, so a repository that never set the key needs no edit.
|
|
19
|
+
A repository that set it to either boolean gets a `TypeError` at load naming the
|
|
20
|
+
mode to write:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
package-build config: `publish.site` is no longer a boolean — write `site: content`. Every package publishes an authored homepage at /<contentPackage>/, so no value means "no web presence": `homepage` publishes that page and nothing else, and `content` publishes it plus every page the content tree compiles to.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Both spellings are **refused rather than mapped** onto the nearest mode. `false`
|
|
27
|
+
read as _this package has no web presence_, which now describes no package at
|
|
28
|
+
all, and a value silently reinterpreted reads to its author as though it still
|
|
29
|
+
means what it said.
|
|
30
|
+
|
|
31
|
+
Nothing else about publishing moved: `publish.address`, `publish.manifests` and
|
|
32
|
+
the whole `site:` block are unchanged, and every address `sohl` and `thalorna`
|
|
33
|
+
already publish is byte-identical across the upgrade.
|
|
34
|
+
|
|
35
|
+
## 2. Author a homepage (optional here, required by #52)
|
|
36
|
+
|
|
37
|
+
Every package is reachable at `https://www.heroiclands.org/<contentPackage>/`,
|
|
38
|
+
and the page there is a note in the content tree:
|
|
39
|
+
|
|
40
|
+
```markdown
|
|
41
|
+
---
|
|
42
|
+
type: homepage
|
|
43
|
+
title: HârnMaster Kethira Basic # optional; defaults to packageBuild.manifest.title
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
What the module is, which system it needs, how to install it.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It compiles into no compendium document, appears in no pack and in no link
|
|
50
|
+
manifest, and is addressed by the package rather than by its own name. It is
|
|
51
|
+
written to the root of `site.out`, one level above the content mount.
|
|
52
|
+
|
|
53
|
+
A repository with no homepage note publishes none, and the site build says so in
|
|
54
|
+
its count. Requiring exactly one is a separate change (#52).
|
|
55
|
+
|
|
56
|
+
## 3. What homepage-only means
|
|
57
|
+
|
|
58
|
+
`homepage` mode does not merely leave the content configuration unused — it
|
|
59
|
+
**fences the content surfaces off**. The tree is never walked for pages, and
|
|
60
|
+
`site.sections`, `site.trees`, `site.landing` and `site.backfillSections` emit
|
|
61
|
+
nothing even when they are declared.
|
|
62
|
+
|
|
63
|
+
That is deliberate, and it is a licensing requirement rather than a preference.
|
|
64
|
+
`sohl-kethira-basic` (Keléstia Productions' Fan Material Guidelines) and
|
|
65
|
+
`harn-adventures` (HârnFanon under Lythia's terms) publish a homepage and no
|
|
66
|
+
other page; the failure mode is silent — a `site:` block added later ships
|
|
67
|
+
licensed content with nobody noticing — so the property is asserted by the code
|
|
68
|
+
path rather than left to configuration.
|
|
69
|
+
|
|
70
|
+
`publish.manifests.publish` is a separate decision and stays `false` for both: a
|
|
71
|
+
link manifest is the dependency edge that would stop a module being withdrawable,
|
|
72
|
+
and a homepage is not.
|
|
73
|
+
|
|
1
74
|
# Migrating to `@heroiclands/package-build` 4.0.0
|
|
2
75
|
|
|
3
76
|
**One authoring change: delete `package:` from every content note.** A note's
|
package/bin/content-build.mjs
CHANGED
|
@@ -76,6 +76,9 @@ import { lintFrontmatter } from "../engine/frontmatter-lint.mjs";
|
|
|
76
76
|
// set — an adventure module ships skills, beings and magic swords — so no
|
|
77
77
|
// consumer gets a subset (#19, #20).
|
|
78
78
|
import { NOTE_SCHEMAS } from "../sohl/note-schemas.mjs";
|
|
79
|
+
// The engine's own types, merged under the registry's so the vocabulary stands
|
|
80
|
+
// in a package that configures no `itemBuilders` at all (#51).
|
|
81
|
+
import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
|
|
79
82
|
import { checkFormatting, lintMarkdown } from "../engine/prose-lint.mjs";
|
|
80
83
|
import { emitLinkManifest } from "../engine/manifest-emit.mjs";
|
|
81
84
|
import {
|
|
@@ -346,7 +349,7 @@ function lintCommand() {
|
|
|
346
349
|
skipDirectories: config.skipDirectories,
|
|
347
350
|
});
|
|
348
351
|
const frontmatter = lintFrontmatter(index, {
|
|
349
|
-
schemas: NOTE_SCHEMAS,
|
|
352
|
+
schemas: { ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS },
|
|
350
353
|
references: argv.references,
|
|
351
354
|
});
|
|
352
355
|
|
|
@@ -823,7 +826,8 @@ function siteCommand() {
|
|
|
823
826
|
|
|
824
827
|
const s = result.stats;
|
|
825
828
|
log.info(
|
|
826
|
-
`wrote ${s.
|
|
829
|
+
`wrote ${s.homepages ?? 0} homepage(s) + ` +
|
|
830
|
+
`${s.content ?? 0} content page(s) + ` +
|
|
827
831
|
`${s.tree ?? 0} tree page(s) + ${s.landings} ` +
|
|
828
832
|
`landing(s) to ${path.relative(process.cwd(), s.out)}`,
|
|
829
833
|
);
|
package/content-config.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* assets:
|
|
34
34
|
* - { from: assets/icons, to: assets/icons }
|
|
35
35
|
* publish:
|
|
36
|
-
* site:
|
|
36
|
+
* site: content
|
|
37
37
|
* manifests: { publish: true, consume: true }
|
|
38
38
|
* ```
|
|
39
39
|
*
|
|
@@ -153,6 +153,62 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
153
153
|
landing: "readme",
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* How much of a package reaches the web.
|
|
158
|
+
*
|
|
159
|
+
* Every HeroicLands package publishes something: a top-level, human-authored
|
|
160
|
+
* homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
|
|
161
|
+
* module is, which system it needs and how to install it (#50). So there is no
|
|
162
|
+
* value here meaning *no web presence at all* — homepage-only is the **floor**,
|
|
163
|
+
* and the default.
|
|
164
|
+
*
|
|
165
|
+
* - `homepage` — the authored homepage, and **no other page**. The content tree
|
|
166
|
+
* is not walked for pages, `site.sections` / `site.trees` / `site.landing`
|
|
167
|
+
* emit nothing, and link-manifest entries carry no web `path`.
|
|
168
|
+
* - `content` — the homepage *plus* every page the content tree publishes: the
|
|
169
|
+
* knowledgebase, the extra trees, the section landings.
|
|
170
|
+
*
|
|
171
|
+
* **Homepage-only is a first-class mode, not an accommodation.**
|
|
172
|
+
* `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
173
|
+
* Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
|
|
174
|
+
* terms) must each publish a homepage and nothing beneath it — two packages
|
|
175
|
+
* under two different fan-content licences. The boundary is **published
|
|
176
|
+
* content**: journal text, artwork, item descriptions, compiled notes. A
|
|
177
|
+
* human-authored page announcing the module discloses none of it. Because the
|
|
178
|
+
* failure mode is silent — a `site:` block added later ships licensed content
|
|
179
|
+
* with nobody noticing — the mode fences the content surfaces off rather than
|
|
180
|
+
* trusting a configuration to stay empty.
|
|
181
|
+
*
|
|
182
|
+
* This was a boolean until 5.0.0, and `false` read as "no web presence", which
|
|
183
|
+
* no longer describes any package. Both spellings are refused rather than
|
|
184
|
+
* mapped: a value silently reinterpreted reads to its author as though it still
|
|
185
|
+
* means what it said.
|
|
186
|
+
*
|
|
187
|
+
* @typedef {"homepage" | "content"} SiteMode
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The publishing modes {@link PublishSwitches.site} may name, floor first.
|
|
192
|
+
*
|
|
193
|
+
* @satisfies {readonly SiteMode[]}
|
|
194
|
+
*/
|
|
195
|
+
export const SITE_MODES = /** @type {const} */ (["homepage", "content"]);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Whether this package publishes the pages its content tree compiles to.
|
|
199
|
+
*
|
|
200
|
+
* The one question every reader of the mode actually asks — the site build, to
|
|
201
|
+
* decide whether to walk the tree at all, and the link-manifest emitter, to
|
|
202
|
+
* decide whether an entry carries a web `path`. Written once here so the two
|
|
203
|
+
* cannot come to disagree about what a mode means.
|
|
204
|
+
*
|
|
205
|
+
* @param {{publish: {site: SiteMode}}} config - A resolved configuration.
|
|
206
|
+
* @returns {boolean} Whether content pages are published.
|
|
207
|
+
*/
|
|
208
|
+
export function publishesContentPages(config) {
|
|
209
|
+
return config.publish.site === "content";
|
|
210
|
+
}
|
|
211
|
+
|
|
156
212
|
/**
|
|
157
213
|
* @typedef {"systems" | "modules"} PackageKind
|
|
158
214
|
*/
|
|
@@ -300,7 +356,8 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
300
356
|
|
|
301
357
|
/**
|
|
302
358
|
* @typedef {object} PublishSwitches
|
|
303
|
-
* @property {
|
|
359
|
+
* @property {SiteMode} site How much of this package reaches the web.
|
|
360
|
+
* See {@link SITE_MODES}.
|
|
304
361
|
* @property {ManifestSwitches} manifests
|
|
305
362
|
*/
|
|
306
363
|
|
|
@@ -387,8 +444,15 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
387
444
|
|
|
388
445
|
/**
|
|
389
446
|
* @typedef {object} PublishSwitchesInput
|
|
390
|
-
* @property {
|
|
447
|
+
* @property {SiteMode} [site]
|
|
391
448
|
* @property {ManifestSwitchesInput} [manifests]
|
|
449
|
+
* @property {AddressSchemeInput} [address]
|
|
450
|
+
*/
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* @typedef {object} AddressSchemeInput
|
|
454
|
+
* @property {string} [prefix] Where the content tree mounts inside the package.
|
|
455
|
+
* @property {string} [landing] Which note addresses a whole section.
|
|
392
456
|
*/
|
|
393
457
|
|
|
394
458
|
/**
|
|
@@ -454,7 +518,9 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
454
518
|
* which has none to invent.
|
|
455
519
|
* @property {Relationships} [relationships] What this package declares about
|
|
456
520
|
* others, in Foundry's own shape.
|
|
457
|
-
* @property {PublishSwitchesInput} [publish] Publishing switches.
|
|
521
|
+
* @property {PublishSwitchesInput} [publish] Publishing switches. The manifest
|
|
522
|
+
* switches default to off; `site`
|
|
523
|
+
* defaults to `homepage`, the floor.
|
|
458
524
|
*/
|
|
459
525
|
|
|
460
526
|
/**
|
|
@@ -1272,6 +1338,43 @@ function normalizeItemBuilders(value) {
|
|
|
1272
1338
|
};
|
|
1273
1339
|
}
|
|
1274
1340
|
|
|
1341
|
+
/**
|
|
1342
|
+
* The publishing mode, refusing the boolean this setting used to be.
|
|
1343
|
+
*
|
|
1344
|
+
* A boolean is refused rather than mapped onto the nearest mode, because the
|
|
1345
|
+
* reading `false` invited — *this package has no web presence* — is exactly the
|
|
1346
|
+
* belief the change exists to correct, and a value quietly reinterpreted reads
|
|
1347
|
+
* to its author as though it still means what it said. So the message names the
|
|
1348
|
+
* mode to write instead of the value to fix.
|
|
1349
|
+
*
|
|
1350
|
+
* @param {unknown} value - The authored `publish.site`.
|
|
1351
|
+
* @returns {SiteMode} The mode.
|
|
1352
|
+
*/
|
|
1353
|
+
function normalizeSiteMode(value) {
|
|
1354
|
+
if (value === undefined) return "homepage";
|
|
1355
|
+
if (typeof value === "boolean") {
|
|
1356
|
+
fail(
|
|
1357
|
+
"publish.site",
|
|
1358
|
+
`is no longer a boolean — write \`site: ${value ? "content" : "homepage"}\`. ` +
|
|
1359
|
+
`Every package publishes an authored homepage at ` +
|
|
1360
|
+
`/<contentPackage>/, so no value means "no web presence": ` +
|
|
1361
|
+
`\`homepage\` publishes that page and nothing else, and ` +
|
|
1362
|
+
`\`content\` publishes it plus every page the content tree ` +
|
|
1363
|
+
`compiles to`,
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
if (
|
|
1367
|
+
typeof value !== "string" ||
|
|
1368
|
+
!(/** @type {readonly string[]} */ (SITE_MODES).includes(value))
|
|
1369
|
+
) {
|
|
1370
|
+
fail(
|
|
1371
|
+
"publish.site",
|
|
1372
|
+
`must be one of ${SITE_MODES.join(", ")} (got ${JSON.stringify(value)})`,
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
return /** @type {SiteMode} */ (value);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1275
1378
|
/**
|
|
1276
1379
|
* @param {unknown} value
|
|
1277
1380
|
* @returns {Readonly<PublishSwitches>}
|
|
@@ -1279,7 +1382,7 @@ function normalizeItemBuilders(value) {
|
|
|
1279
1382
|
function normalizePublish(value) {
|
|
1280
1383
|
if (value === undefined) {
|
|
1281
1384
|
return Object.freeze({
|
|
1282
|
-
site:
|
|
1385
|
+
site: "homepage",
|
|
1283
1386
|
manifests: Object.freeze({ publish: false, consume: false }),
|
|
1284
1387
|
address: Object.freeze({ ...DEFAULT_ADDRESS_SCHEME }),
|
|
1285
1388
|
});
|
|
@@ -1332,7 +1435,7 @@ function normalizePublish(value) {
|
|
|
1332
1435
|
}
|
|
1333
1436
|
|
|
1334
1437
|
return Object.freeze({
|
|
1335
|
-
site:
|
|
1438
|
+
site: normalizeSiteMode(publish.site),
|
|
1336
1439
|
address: Object.freeze({ prefix, landing }),
|
|
1337
1440
|
manifests: Object.freeze({
|
|
1338
1441
|
publish: optionalBoolean(
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The package homepage — a note that compiles to a **page** rather than to a
|
|
16
|
+
* compendium document (#51).
|
|
17
|
+
*
|
|
18
|
+
* Every HeroicLands package is reachable at `https://www.heroiclands.org/<contentPackage>/`,
|
|
19
|
+
* and what a reader finds there is one markdown file in the content tree,
|
|
20
|
+
* written by a person: what the module is, which system it needs, how to install
|
|
21
|
+
* it, where its source lives. Nothing about it is derived.
|
|
22
|
+
*
|
|
23
|
+
* **Authored, not assembled.** An earlier sketch generated the page in tiers —
|
|
24
|
+
* identity and licence from the manifest, install URL from the release address,
|
|
25
|
+
* "requires" links from `relationships`, a card per configured section. It would
|
|
26
|
+
* have worked and needed almost no authoring, and it produces a page nobody
|
|
27
|
+
* chose the contents of. The things that matter most on these pages cannot be
|
|
28
|
+
* derived: that Kethira requires buying the book from Keléstia, what Thalorna's
|
|
29
|
+
* setting *is*, which of twenty sections a reader should start with. So the only
|
|
30
|
+
* thing defaulted here is the title, from `packageBuild.manifest.title`, so that
|
|
31
|
+
* the package's name is not written twice.
|
|
32
|
+
*
|
|
33
|
+
* **Dispatched by `type`, not by filename.** A fixed `homepage.md` the walker
|
|
34
|
+
* special-cased would be the anomaly: notes are routed by frontmatter, not by
|
|
35
|
+
* location, and `NOTE_SCHEMAS` already routes `doc`, `macro`, `being` and the
|
|
36
|
+
* map types. `homepage` is one more entry whose compile step emits a page. It is
|
|
37
|
+
* deliberately not `README.md`: `landing: readme` already means "a `README.md`
|
|
38
|
+
* is its section's landing page", and `sohl-thalorna/assets/content/README.md`
|
|
39
|
+
* is a developer explainer about the source tree — adopting that name would make
|
|
40
|
+
* Thalorna's public front page its build documentation.
|
|
41
|
+
*
|
|
42
|
+
* **Engine, not `sohl/`.** The `engine/` ÷ `sohl/` line separates *note-format*
|
|
43
|
+
* knowledge from *game-system* knowledge, and a homepage is note format: it
|
|
44
|
+
* carries no `system` block, mirrors no item builder, and would mean the same
|
|
45
|
+
* thing for a game system that is not SoHL. Reachability is the symptom that
|
|
46
|
+
* makes it obvious — `HarnMaster-3-FoundryVTT` declares no `itemBuilders`, so a
|
|
47
|
+
* type living in the SoHL registry would be unavailable to HM3 and to every HM3
|
|
48
|
+
* module, which is most of the packages that need a homepage and nothing else.
|
|
49
|
+
*
|
|
50
|
+
* **Its address is the package's, not the note's.** A homepage publishes at
|
|
51
|
+
* `/<contentPackage>/` because that is where the package is, so `name.full`,
|
|
52
|
+
* `shortcode` and `id` decide nothing on it (#53 refuses them outright; this
|
|
53
|
+
* module simply never reads them). It compiles into no document, so it carries
|
|
54
|
+
* no compendium UUID and appears in no pack and in no link-manifest entry.
|
|
55
|
+
*
|
|
56
|
+
* @module
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The note type that compiles to the package homepage.
|
|
61
|
+
*
|
|
62
|
+
* @type {string}
|
|
63
|
+
*/
|
|
64
|
+
export const HOMEPAGE_TYPE = "homepage";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What a homepage note may write under `sohl:` — nothing.
|
|
68
|
+
*
|
|
69
|
+
* Empty on purpose, and declared rather than omitted: a type with no vocabulary
|
|
70
|
+
* and a type that is unknown are different findings, and only the second is an
|
|
71
|
+
* authoring error. The whole envelope is the two top-level keys `type` and an
|
|
72
|
+
* optional `title`; there is no game-system data on a page that compiles to no
|
|
73
|
+
* document.
|
|
74
|
+
*
|
|
75
|
+
* @type {readonly import("./field-spec.mjs").FieldSpec[]}
|
|
76
|
+
*/
|
|
77
|
+
export const HOMEPAGE_FIELDS = Object.freeze([]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Where a homepage is written, relative to the package's site root.
|
|
81
|
+
*
|
|
82
|
+
* Hugo's section landing, because the page *is* the package's landing: the
|
|
83
|
+
* package root is a section and this is its index.
|
|
84
|
+
*
|
|
85
|
+
* @type {string}
|
|
86
|
+
*/
|
|
87
|
+
export const HOMEPAGE_DESTINATION = "_index.md";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether a note's frontmatter declares the homepage type.
|
|
91
|
+
*
|
|
92
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
93
|
+
* @returns {boolean} Whether it is a homepage note.
|
|
94
|
+
*/
|
|
95
|
+
export function isHomepage(fm) {
|
|
96
|
+
return Boolean(fm) && fm.type === HOMEPAGE_TYPE;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The title a homepage publishes under.
|
|
101
|
+
*
|
|
102
|
+
* The one defaulted value on the page, and it defaults to the package's own
|
|
103
|
+
* `packageBuild.manifest.title` — the name Foundry already shows for the
|
|
104
|
+
* package — so a homepage that adds nothing to it need not restate it. An
|
|
105
|
+
* authored `title` wins, because a front page is allowed to greet a reader
|
|
106
|
+
* differently from a package browser.
|
|
107
|
+
*
|
|
108
|
+
* Falls back to `contentPackage` last, so a package that has no manifest of its
|
|
109
|
+
* own still yields a titled page rather than a blank heading.
|
|
110
|
+
*
|
|
111
|
+
* @param {object|null|undefined} fm - The note's frontmatter.
|
|
112
|
+
* @param {object} config - The resolved configuration.
|
|
113
|
+
* @returns {string} The title.
|
|
114
|
+
*/
|
|
115
|
+
export function homepageTitle(fm, config) {
|
|
116
|
+
const authored = fm?.title;
|
|
117
|
+
if (typeof authored === "string" && authored.trim()) return authored;
|
|
118
|
+
const manifest = /** @type {Record<string, unknown>|undefined} */ (
|
|
119
|
+
config?.packageBuild?.manifest
|
|
120
|
+
);
|
|
121
|
+
const title = manifest?.title;
|
|
122
|
+
return typeof title === "string" && title.trim() ?
|
|
123
|
+
title
|
|
124
|
+
: config.contentPackage;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The frontmatter a homepage publishes with.
|
|
129
|
+
*
|
|
130
|
+
* The note's own, plus the two derived values every emitted page carries: the
|
|
131
|
+
* resolved `title`, and the package the build derived — no note declares one
|
|
132
|
+
* (`package:` is retired, #56) and the theme's breadcrumb partial reads
|
|
133
|
+
* `.Params.package`.
|
|
134
|
+
*
|
|
135
|
+
* An authored `aliases` is dropped for the same reason it is on every other
|
|
136
|
+
* page: Obsidian reads it as names a reader might call the note, Hugo reads it
|
|
137
|
+
* as URL redirects, and passing it through would publish a redirect stub at
|
|
138
|
+
* each one.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} fm - The note's frontmatter.
|
|
141
|
+
* @param {object} options - Options.
|
|
142
|
+
* @param {string} options.contentPackage - The package this build publishes.
|
|
143
|
+
* @param {string} options.title - The resolved title.
|
|
144
|
+
* @returns {object} The frontmatter to write.
|
|
145
|
+
*/
|
|
146
|
+
export function homepageFrontmatter(fm, { contentPackage, title }) {
|
|
147
|
+
const data = { ...fm, package: contentPackage, title };
|
|
148
|
+
delete data.aliases;
|
|
149
|
+
return data;
|
|
150
|
+
}
|
package/engine/index.mjs
CHANGED
|
@@ -60,6 +60,12 @@ export * as notePackage from "./note-package.mjs";
|
|
|
60
60
|
/** Frontmatter fields a note may no longer declare, and the refusal of them. */
|
|
61
61
|
export * as retiredFields from "./retired-fields.mjs";
|
|
62
62
|
|
|
63
|
+
/** The package homepage: the note type that compiles to a page, not a document. */
|
|
64
|
+
export * as homepage from "./homepage.mjs";
|
|
65
|
+
|
|
66
|
+
/** The note types the engine itself declares, whatever a consumer registers. */
|
|
67
|
+
export * as noteSchemas from "./note-schemas.mjs";
|
|
68
|
+
|
|
63
69
|
/** The shipped Foundry manifest: locating it, reading it, guarding its id. */
|
|
64
70
|
|
|
65
71
|
/** The URL a content note is published at — the one web-address rule. */
|
package/engine/manifest-emit.mjs
CHANGED
|
@@ -59,6 +59,7 @@ import { assertNoDraftField } from "./retired-fields.mjs";
|
|
|
59
59
|
import { journalPageId, splitPages } from "./journals.mjs";
|
|
60
60
|
import { routerFor } from "./pack-router.mjs";
|
|
61
61
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
62
|
+
import { publishesContentPages } from "../content-config.mjs";
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* The reserved anchor name for a journal's **first** page.
|
|
@@ -271,7 +272,7 @@ export function manifestContext(config = loadPackConfig()) {
|
|
|
271
272
|
foundryPackageId: config.foundryPackage,
|
|
272
273
|
packRouter: routerFor(config),
|
|
273
274
|
scheme: config.publish.address,
|
|
274
|
-
web: config
|
|
275
|
+
web: publishesContentPages(config),
|
|
275
276
|
// The walk's own configuration, threaded through rather than left to
|
|
276
277
|
// its default, so a caller that passes a config drives every read.
|
|
277
278
|
skipDirectories: config.skipDirectories,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The note types the **engine** declares — the ones whose vocabulary is a fact
|
|
16
|
+
* about the note format rather than about any game system (#51).
|
|
17
|
+
*
|
|
18
|
+
* `sohl/note-schemas.mjs` is the other half, and the line between them is the
|
|
19
|
+
* `engine/` ÷ `sohl/` line everywhere else in this package: note-format
|
|
20
|
+
* knowledge here, game-system knowledge there. It is not a permission boundary
|
|
21
|
+
* between consumers — every content project authors the full vocabulary — but it
|
|
22
|
+
* is a reachability one in exactly one direction. A package that declares no
|
|
23
|
+
* `itemBuilders` (`HarnMaster-3-FoundryVTT`, and every HM3 module) uses only the
|
|
24
|
+
* packaging half of the toolchain, so a type declared in the SoHL registry would
|
|
25
|
+
* be unavailable to it. These are the types every package has, whatever it
|
|
26
|
+
* ships.
|
|
27
|
+
*
|
|
28
|
+
* One entry today. A consumer merges it under its own registry —
|
|
29
|
+
* `{ ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS }` — so a game system may extend
|
|
30
|
+
* these but the engine's declaration stands wherever no registry is configured.
|
|
31
|
+
*
|
|
32
|
+
* @module
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { HOMEPAGE_FIELDS, HOMEPAGE_TYPE } from "./homepage.mjs";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Every engine-level content type, and what a note of that type may write.
|
|
39
|
+
*
|
|
40
|
+
* @type {Readonly<Record<string, readonly import("./field-spec.mjs").FieldSpec[]>>}
|
|
41
|
+
*/
|
|
42
|
+
export const ENGINE_NOTE_SCHEMAS = Object.freeze({
|
|
43
|
+
[HOMEPAGE_TYPE]: HOMEPAGE_FIELDS,
|
|
44
|
+
});
|
package/engine/site-build.mjs
CHANGED
|
@@ -63,6 +63,13 @@ import {
|
|
|
63
63
|
import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
|
|
64
64
|
import { loadPackConfig } from "./pack-config.mjs";
|
|
65
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
|
|
|
@@ -137,6 +144,10 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
137
144
|
// (#56).
|
|
138
145
|
const pkg = ctx.contentPackage;
|
|
139
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;
|
|
140
151
|
|
|
141
152
|
for (const hit of frontmatterWikilinks(fm)) {
|
|
142
153
|
fmLinkFindings.push({ file, ...hit });
|
|
@@ -249,6 +260,62 @@ export function collectTreePages(tree, ctx) {
|
|
|
249
260
|
return { pages, fmLinkFindings };
|
|
250
261
|
}
|
|
251
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
|
+
|
|
252
319
|
/**
|
|
253
320
|
* The integrity gates a site build runs before it writes anything.
|
|
254
321
|
*
|
|
@@ -318,6 +385,30 @@ export function siteGates(pages, findings, { manifestDir }) {
|
|
|
318
385
|
return out;
|
|
319
386
|
}
|
|
320
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
|
+
|
|
321
412
|
/** Whether any gate produced a finding. */
|
|
322
413
|
export function gatesFailed(gates) {
|
|
323
414
|
return Boolean(
|
|
@@ -706,6 +797,10 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
706
797
|
const resolved = config ?? loadPackConfig();
|
|
707
798
|
const site = resolved.site;
|
|
708
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);
|
|
709
804
|
|
|
710
805
|
// Where the package is served, and where its content mounts inside it. The
|
|
711
806
|
// two are separate facts: `base` is the package's own address on the site
|
|
@@ -722,9 +817,16 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
722
817
|
// directory it was launched from (#1508).
|
|
723
818
|
const outBase = resolveOutputRoot(resolved.rootDir, site.out);
|
|
724
819
|
const out =
|
|
725
|
-
outRoot ?
|
|
726
|
-
|
|
727
|
-
|
|
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;
|
|
728
830
|
|
|
729
831
|
const packages = new Set(
|
|
730
832
|
site.packages.length ? site.packages : [resolved.contentPackage],
|
|
@@ -744,6 +846,25 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
744
846
|
// note was deleted or renamed would otherwise linger and keep publishing.
|
|
745
847
|
fs.rmSync(outBase, { recursive: true, force: true });
|
|
746
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
|
+
|
|
747
868
|
const content = collectContentPages(resolved.paths.content, ctx);
|
|
748
869
|
const pages = [...content.pages];
|
|
749
870
|
const fmLinkFindings = [...content.fmLinkFindings];
|
|
@@ -804,12 +925,21 @@ export function buildSite({ config, outRoot } = {}) {
|
|
|
804
925
|
sectionTitle: site.backfillSections ? pluralTitle : null,
|
|
805
926
|
});
|
|
806
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
|
+
|
|
807
932
|
return {
|
|
808
933
|
gates,
|
|
809
934
|
manifests: gates.manifests,
|
|
810
935
|
tableErrors: rendered.tableErrors,
|
|
811
936
|
wikiErrors: rendered.wikiErrors,
|
|
812
|
-
stats: {
|
|
937
|
+
stats: {
|
|
938
|
+
...rendered.byKind,
|
|
939
|
+
homepages: homepagesWritten,
|
|
940
|
+
landings,
|
|
941
|
+
out,
|
|
942
|
+
},
|
|
813
943
|
};
|
|
814
944
|
}
|
|
815
945
|
|
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/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
|
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a note's frontmatter declares the homepage type.
|
|
3
|
+
*
|
|
4
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
5
|
+
* @returns {boolean} Whether it is a homepage note.
|
|
6
|
+
*/
|
|
7
|
+
export function isHomepage(fm: object | null | undefined): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The title a homepage publishes under.
|
|
10
|
+
*
|
|
11
|
+
* The one defaulted value on the page, and it defaults to the package's own
|
|
12
|
+
* `packageBuild.manifest.title` — the name Foundry already shows for the
|
|
13
|
+
* package — so a homepage that adds nothing to it need not restate it. An
|
|
14
|
+
* authored `title` wins, because a front page is allowed to greet a reader
|
|
15
|
+
* differently from a package browser.
|
|
16
|
+
*
|
|
17
|
+
* Falls back to `contentPackage` last, so a package that has no manifest of its
|
|
18
|
+
* own still yields a titled page rather than a blank heading.
|
|
19
|
+
*
|
|
20
|
+
* @param {object|null|undefined} fm - The note's frontmatter.
|
|
21
|
+
* @param {object} config - The resolved configuration.
|
|
22
|
+
* @returns {string} The title.
|
|
23
|
+
*/
|
|
24
|
+
export function homepageTitle(fm: object | null | undefined, config: object): string;
|
|
25
|
+
/**
|
|
26
|
+
* The frontmatter a homepage publishes with.
|
|
27
|
+
*
|
|
28
|
+
* The note's own, plus the two derived values every emitted page carries: the
|
|
29
|
+
* resolved `title`, and the package the build derived — no note declares one
|
|
30
|
+
* (`package:` is retired, #56) and the theme's breadcrumb partial reads
|
|
31
|
+
* `.Params.package`.
|
|
32
|
+
*
|
|
33
|
+
* An authored `aliases` is dropped for the same reason it is on every other
|
|
34
|
+
* page: Obsidian reads it as names a reader might call the note, Hugo reads it
|
|
35
|
+
* as URL redirects, and passing it through would publish a redirect stub at
|
|
36
|
+
* each one.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} fm - The note's frontmatter.
|
|
39
|
+
* @param {object} options - Options.
|
|
40
|
+
* @param {string} options.contentPackage - The package this build publishes.
|
|
41
|
+
* @param {string} options.title - The resolved title.
|
|
42
|
+
* @returns {object} The frontmatter to write.
|
|
43
|
+
*/
|
|
44
|
+
export function homepageFrontmatter(fm: object, { contentPackage, title }: {
|
|
45
|
+
contentPackage: string;
|
|
46
|
+
title: string;
|
|
47
|
+
}): object;
|
|
48
|
+
/**
|
|
49
|
+
* The package homepage — a note that compiles to a **page** rather than to a
|
|
50
|
+
* compendium document (#51).
|
|
51
|
+
*
|
|
52
|
+
* Every HeroicLands package is reachable at `https://www.heroiclands.org/<contentPackage>/`,
|
|
53
|
+
* and what a reader finds there is one markdown file in the content tree,
|
|
54
|
+
* written by a person: what the module is, which system it needs, how to install
|
|
55
|
+
* it, where its source lives. Nothing about it is derived.
|
|
56
|
+
*
|
|
57
|
+
* **Authored, not assembled.** An earlier sketch generated the page in tiers —
|
|
58
|
+
* identity and licence from the manifest, install URL from the release address,
|
|
59
|
+
* "requires" links from `relationships`, a card per configured section. It would
|
|
60
|
+
* have worked and needed almost no authoring, and it produces a page nobody
|
|
61
|
+
* chose the contents of. The things that matter most on these pages cannot be
|
|
62
|
+
* derived: that Kethira requires buying the book from Keléstia, what Thalorna's
|
|
63
|
+
* setting *is*, which of twenty sections a reader should start with. So the only
|
|
64
|
+
* thing defaulted here is the title, from `packageBuild.manifest.title`, so that
|
|
65
|
+
* the package's name is not written twice.
|
|
66
|
+
*
|
|
67
|
+
* **Dispatched by `type`, not by filename.** A fixed `homepage.md` the walker
|
|
68
|
+
* special-cased would be the anomaly: notes are routed by frontmatter, not by
|
|
69
|
+
* location, and `NOTE_SCHEMAS` already routes `doc`, `macro`, `being` and the
|
|
70
|
+
* map types. `homepage` is one more entry whose compile step emits a page. It is
|
|
71
|
+
* deliberately not `README.md`: `landing: readme` already means "a `README.md`
|
|
72
|
+
* is its section's landing page", and `sohl-thalorna/assets/content/README.md`
|
|
73
|
+
* is a developer explainer about the source tree — adopting that name would make
|
|
74
|
+
* Thalorna's public front page its build documentation.
|
|
75
|
+
*
|
|
76
|
+
* **Engine, not `sohl/`.** The `engine/` ÷ `sohl/` line separates *note-format*
|
|
77
|
+
* knowledge from *game-system* knowledge, and a homepage is note format: it
|
|
78
|
+
* carries no `system` block, mirrors no item builder, and would mean the same
|
|
79
|
+
* thing for a game system that is not SoHL. Reachability is the symptom that
|
|
80
|
+
* makes it obvious — `HarnMaster-3-FoundryVTT` declares no `itemBuilders`, so a
|
|
81
|
+
* type living in the SoHL registry would be unavailable to HM3 and to every HM3
|
|
82
|
+
* module, which is most of the packages that need a homepage and nothing else.
|
|
83
|
+
*
|
|
84
|
+
* **Its address is the package's, not the note's.** A homepage publishes at
|
|
85
|
+
* `/<contentPackage>/` because that is where the package is, so `name.full`,
|
|
86
|
+
* `shortcode` and `id` decide nothing on it (#53 refuses them outright; this
|
|
87
|
+
* module simply never reads them). It compiles into no document, so it carries
|
|
88
|
+
* no compendium UUID and appears in no pack and in no link-manifest entry.
|
|
89
|
+
*
|
|
90
|
+
* @module
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* The note type that compiles to the package homepage.
|
|
94
|
+
*
|
|
95
|
+
* @type {string}
|
|
96
|
+
*/
|
|
97
|
+
export const HOMEPAGE_TYPE: string;
|
|
98
|
+
/**
|
|
99
|
+
* What a homepage note may write under `sohl:` — nothing.
|
|
100
|
+
*
|
|
101
|
+
* Empty on purpose, and declared rather than omitted: a type with no vocabulary
|
|
102
|
+
* and a type that is unknown are different findings, and only the second is an
|
|
103
|
+
* authoring error. The whole envelope is the two top-level keys `type` and an
|
|
104
|
+
* optional `title`; there is no game-system data on a page that compiles to no
|
|
105
|
+
* document.
|
|
106
|
+
*
|
|
107
|
+
* @type {readonly import("./field-spec.mjs").FieldSpec[]}
|
|
108
|
+
*/
|
|
109
|
+
export const HOMEPAGE_FIELDS: readonly import("./field-spec.mjs").FieldSpec[];
|
|
110
|
+
/**
|
|
111
|
+
* Where a homepage is written, relative to the package's site root.
|
|
112
|
+
*
|
|
113
|
+
* Hugo's section landing, because the page *is* the package's landing: the
|
|
114
|
+
* package root is a section and this is its index.
|
|
115
|
+
*
|
|
116
|
+
* @type {string}
|
|
117
|
+
*/
|
|
118
|
+
export const HOMEPAGE_DESTINATION: string;
|
package/types/engine/index.d.mts
CHANGED
|
@@ -7,6 +7,8 @@ export * as packRouter from "./pack-router.mjs";
|
|
|
7
7
|
export * as contentPackage from "./content-package.mjs";
|
|
8
8
|
export * as notePackage from "./note-package.mjs";
|
|
9
9
|
export * as retiredFields from "./retired-fields.mjs";
|
|
10
|
+
export * as homepage from "./homepage.mjs";
|
|
11
|
+
export * as noteSchemas from "./note-schemas.mjs";
|
|
10
12
|
export * as contentSlug from "./content-slug.mjs";
|
|
11
13
|
export * as contentAddress from "./content-address.mjs";
|
|
12
14
|
export * as foreignManifests from "./foreign-manifests.mjs";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every engine-level content type, and what a note of that type may write.
|
|
3
|
+
*
|
|
4
|
+
* @type {Readonly<Record<string, readonly import("./field-spec.mjs").FieldSpec[]>>}
|
|
5
|
+
*/
|
|
6
|
+
export const ENGINE_NOTE_SCHEMAS: Readonly<Record<string, readonly import("./field-spec.mjs").FieldSpec[]>>;
|
|
@@ -41,6 +41,43 @@ export function collectTreePages(tree: object, ctx: object): {
|
|
|
41
41
|
pages: object[];
|
|
42
42
|
fmLinkFindings: object[];
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* The package's homepage notes — the authored page at `/<contentPackage>/`.
|
|
46
|
+
*
|
|
47
|
+
* A separate walk from {@link collectContentPages} rather than a branch inside
|
|
48
|
+
* it, because in homepage-only mode it is the **whole** of the site build: the
|
|
49
|
+
* content tree is never read for pages at all, so the licensing constraint two
|
|
50
|
+
* packages ship under is a property of the code path rather than of a
|
|
51
|
+
* configuration that happens to be empty (#55).
|
|
52
|
+
*
|
|
53
|
+
* Returned as a list rather than as the one note there should be. Requiring
|
|
54
|
+
* exactly one is #52's, and it is a separate decision — this reports what it
|
|
55
|
+
* found so a count is visible either way.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} contentBase - Absolute path to the content tree.
|
|
58
|
+
* @param {object} ctx - `{ skipDirectories }`.
|
|
59
|
+
* @returns {{pages: object[]}} The homepage notes, in walk order.
|
|
60
|
+
*/
|
|
61
|
+
export function collectHomepages(contentBase: string, ctx: object): {
|
|
62
|
+
pages: object[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Writes each homepage at the package's own root.
|
|
66
|
+
*
|
|
67
|
+
* Its own writer, deliberately small. A homepage is authored markdown published
|
|
68
|
+
* verbatim — no table expansion, no section landing, and (until #54) no link
|
|
69
|
+
* resolution — so routing it through {@link renderPages} would buy it a pipeline
|
|
70
|
+
* it has no input for, and would make homepage-only mode depend on the index,
|
|
71
|
+
* the foreign manifests and the table universe that mode exists to not build.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} outRoot - The package's site root — the configured `site.out`,
|
|
74
|
+
* one level above the content mount.
|
|
75
|
+
* @param {readonly object[]} pages - From {@link collectHomepages}.
|
|
76
|
+
* @param {object} config - The resolved configuration, for the package name and
|
|
77
|
+
* the default title.
|
|
78
|
+
* @returns {number} How many pages were written.
|
|
79
|
+
*/
|
|
80
|
+
export function writeHomepages(outRoot: string, pages: readonly object[], config: object): number;
|
|
44
81
|
/**
|
|
45
82
|
* The integrity gates a site build runs before it writes anything.
|
|
46
83
|
*
|
|
@@ -63,6 +100,17 @@ export function collectTreePages(tree: object, ctx: object): {
|
|
|
63
100
|
* @returns {object} The gate results and, when they pass, the built index.
|
|
64
101
|
*/
|
|
65
102
|
export function siteGates(pages: object[], findings: object, { manifestDir }: object): object;
|
|
103
|
+
/**
|
|
104
|
+
* The gate result of a build that ran none of them.
|
|
105
|
+
*
|
|
106
|
+
* Homepage-only publishes one authored page and resolves nothing, so every gate
|
|
107
|
+
* here is about a surface that mode does not have. The shape is returned all the
|
|
108
|
+
* same, because a caller reads the same fields whichever mode ran and a `null`
|
|
109
|
+
* would make each of them a special case.
|
|
110
|
+
*
|
|
111
|
+
* @returns {object} An all-clear gate result.
|
|
112
|
+
*/
|
|
113
|
+
export function emptyGates(): object;
|
|
66
114
|
/** Whether any gate produced a finding. */
|
|
67
115
|
export function gatesFailed(gates: any): boolean;
|
|
68
116
|
/**
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Every content type this package compiles, and what a note of that type may
|
|
3
3
|
* write.
|
|
4
4
|
*
|
|
5
|
+
* The engine's own types are merged in first, so a SoHL tree is checked against
|
|
6
|
+
* one vocabulary rather than two. They are declared there rather than here
|
|
7
|
+
* because they are note-format knowledge — a `homepage` carries no `system`
|
|
8
|
+
* block and would mean the same thing for a game system that is not SoHL — and
|
|
9
|
+
* because a package declaring no `itemBuilders` never reaches this file (#51).
|
|
10
|
+
*
|
|
5
11
|
* @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
|
|
6
12
|
*/
|
|
7
13
|
export const NOTE_SCHEMAS: Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>;
|