@heroiclands/package-build 22.4.3 → 22.4.5

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +28 -18
  2. package/CONTENT.md +77 -1
  3. package/README.md +9 -0
  4. package/bin/content-build.mjs +143 -0
  5. package/bin/package-build.mjs +18 -4
  6. package/content-config.mjs +7 -0
  7. package/docs/api.md +84 -0
  8. package/docs/commands.md +182 -2
  9. package/docs/configuration.md +19 -1
  10. package/docs/content-format.md +105 -6
  11. package/docs/diagnostics.md +1 -0
  12. package/docs/project-setup.md +7 -6
  13. package/engine/changelog-group.mjs +27 -6
  14. package/engine/changelog-lint.mjs +27 -7
  15. package/engine/frontmatter-lint.mjs +11 -1
  16. package/engine/homepage.mjs +9 -1
  17. package/engine/index.mjs +18 -0
  18. package/engine/map-build.mjs +240 -0
  19. package/engine/map-dot.mjs +685 -0
  20. package/engine/map-graphviz.mjs +138 -0
  21. package/engine/map-layout.mjs +712 -0
  22. package/engine/map-places.mjs +561 -0
  23. package/engine/metadata-index.mjs +7 -0
  24. package/engine/note-vocabulary.mjs +28 -0
  25. package/engine/place-relations.mjs +637 -0
  26. package/engine/related-pages.mjs +161 -0
  27. package/engine/site-build.mjs +107 -9
  28. package/engine/site-config.mjs +1 -1
  29. package/engine/site-index.mjs +5 -1
  30. package/engine/site-root.mjs +147 -6
  31. package/engine/web-wikilinks.mjs +13 -2
  32. package/package.json +3 -2
  33. package/types/engine/changelog-lint.d.mts +9 -7
  34. package/types/engine/homepage.d.mts +8 -1
  35. package/types/engine/index.d.mts +6 -0
  36. package/types/engine/map-build.d.mts +78 -0
  37. package/types/engine/map-dot.d.mts +130 -0
  38. package/types/engine/map-graphviz.d.mts +61 -0
  39. package/types/engine/map-layout.d.mts +359 -0
  40. package/types/engine/map-places.d.mts +259 -0
  41. package/types/engine/note-vocabulary.d.mts +12 -0
  42. package/types/engine/place-relations.d.mts +95 -0
  43. package/types/engine/related-pages.d.mts +70 -0
  44. package/types/engine/site-build.d.mts +36 -3
  45. package/types/engine/site-config.d.mts +1 -1
  46. package/types/engine/site-index.d.mts +5 -1
  47. package/types/engine/site-root.d.mts +59 -7
  48. package/types/engine/web-wikilinks.d.mts +11 -2
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The bearing the other end of a relation states, or `undefined` for a value
3
+ * that is not a bearing.
4
+ *
5
+ * @param {unknown} bearing - A bearing.
6
+ * @returns {string|undefined} The one four steps around the compass.
7
+ */
8
+ export function oppositeBearing(bearing: unknown): string | undefined;
9
+ /**
10
+ * Check a place note's `data.borders`.
11
+ *
12
+ * Every `to` is a shortcode a place declares, every `bearing` is one of the
13
+ * eight, no pair is listed twice, no target is the note's own parent or child,
14
+ * and the other end states the pair back at the opposite bearing — a missing
15
+ * reciprocal is a warning naming both notes, a wrong one an error.
16
+ *
17
+ * @param {object} note - The note, as the link index hands it over.
18
+ * @param {object} [opts]
19
+ * @param {object} [opts.index] - The link index, for resolving `to`.
20
+ * @returns {object[]} Findings.
21
+ */
22
+ export function checkBorders(note: object, { index }?: {
23
+ index?: object | undefined;
24
+ }): object[];
25
+ /**
26
+ * Check a place note's `data.routes`.
27
+ *
28
+ * As {@link checkBorders}, plus: `mode` and `days` are required and from
29
+ * their sets, `terrain` is from the registry and crossable by the mode, a pair
30
+ * appears once per mode, and the other end states the pair back by the same
31
+ * mode, at the opposite bearing, with the same days.
32
+ *
33
+ * @param {object} note - The note, as the link index hands it over.
34
+ * @param {object} [opts]
35
+ * @param {object} [opts.index] - The link index, for resolving `to`.
36
+ * @returns {object[]} Findings.
37
+ */
38
+ export function checkRoutes(note: object, { index }?: {
39
+ index?: object | undefined;
40
+ }): object[];
41
+ /**
42
+ * The eight compass bearings, clockwise from north.
43
+ *
44
+ * Where a neighbour or a destination lies from the place stating it. The
45
+ * order is load-bearing: {@link oppositeBearing} reads four steps around it.
46
+ *
47
+ * @type {readonly string[]}
48
+ */
49
+ export const BEARINGS: readonly string[];
50
+ /**
51
+ * The modes a route is travelled by: `land`, `boat` on a river or a lake,
52
+ * `ship` on open sea.
53
+ *
54
+ * @type {readonly string[]}
55
+ */
56
+ export const ROUTE_MODES: readonly string[];
57
+ /**
58
+ * The days markers a route may state.
59
+ *
60
+ * Each is "about this, under normal conditions": `10`, `20` and `30` are one,
61
+ * two and three ten-day weeks, `180` is many months, `360` is "who knows".
62
+ *
63
+ * @type {readonly number[]}
64
+ */
65
+ export const TRAVEL_DAYS: readonly number[];
66
+ /**
67
+ * The terrain registry, each terrain naming the modes that cross it.
68
+ *
69
+ * A land terrain is crossed by land alone; open sea by ship; a river or a lake
70
+ * by boat; a coast by all three, because a coast road and a coasting voyage
71
+ * are both journeys along it.
72
+ *
73
+ * @type {Readonly<Record<string, readonly string[]>>}
74
+ */
75
+ export const TERRAIN_MODES: Readonly<Record<string, readonly string[]>>;
76
+ /**
77
+ * The terrain names, in registry order — the closed set a `terrain` list
78
+ * draws from.
79
+ *
80
+ * @type {readonly string[]}
81
+ */
82
+ export const TERRAINS: readonly string[];
83
+ /**
84
+ * The keys a `borders` entry may carry. Both are required.
85
+ *
86
+ * @type {readonly string[]}
87
+ */
88
+ export const BORDER_KEYS: readonly string[];
89
+ /**
90
+ * The keys a `routes` entry may carry. `to`, `bearing`, `mode` and `days` are
91
+ * required; `terrain` and `leagues` are optional.
92
+ *
93
+ * @type {readonly string[]}
94
+ */
95
+ export const ROUTE_KEYS: readonly string[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Inverts a link graph into each page's backlinks and mentions.
3
+ *
4
+ * An edge is `(source URL, target URL)`, and a URL that is not in `entries` is
5
+ * not a page of this site, so an edge touching one is dropped. A self-link says
6
+ * nothing a reader is not already looking at, and a page that links another
7
+ * three times is connected to it once, so both collapse.
8
+ *
9
+ * The result holds only pages with at least one connection, so the caller can
10
+ * write nothing on a page with none — the theme's silent-disappear convention,
11
+ * kept at the source rather than papered over with an empty block.
12
+ *
13
+ * @param {Iterable<readonly [string, string]>} edges - Every resolved link, as
14
+ * the source page's URL and the target page's.
15
+ * @param {ReadonlyMap<string, RelatedEntry>} entries - Every page of this
16
+ * site, keyed by URL.
17
+ * @returns {Map<string, Related>} URL → the page's `related` block, both lists
18
+ * sorted, for every page with at least one entry in either.
19
+ */
20
+ export function relatedPages(edges: Iterable<readonly [string, string]>, entries: ReadonlyMap<string, RelatedEntry>): Map<string, Related>;
21
+ /**
22
+ * The pages a homepage's body links to, as URLs.
23
+ *
24
+ * A homepage is published verbatim — no wikilink on it resolves, and its links
25
+ * are ordinary markdown the browser resolves against the page's own address,
26
+ * which is the package root. So its edges are read the same way: each target
27
+ * resolved against `base`, the way a browser would. A package-relative
28
+ * `being-x/`, a root-relative `/pkg/being-x/` and a `./being-x/#anchor` all
29
+ * name the same page; a bare `#anchor` names the homepage itself.
30
+ *
31
+ * A link that names a host or a scheme leaves the site, whatever path it
32
+ * carries, and yields nothing: the site's own pages are addressed without one.
33
+ *
34
+ * @param {string} body - The homepage's markdown body.
35
+ * @param {string} base - Where the package is served — the homepage's own URL.
36
+ * @returns {string[]} The targets' URLs, in body order, repeats included.
37
+ */
38
+ export function homepageLinkTargets(body: string, base: string): string[];
39
+ /**
40
+ * One entry in a `related` list — the page a reader is pointed at.
41
+ */
42
+ export type RelatedEntry = {
43
+ /**
44
+ * The page's published title.
45
+ */
46
+ title: string;
47
+ /**
48
+ * `<base><slug>/` — the page's address as every href
49
+ * this build renders composes it, never the
50
+ * site-root-relative form the page states for itself.
51
+ */
52
+ url: string;
53
+ /**
54
+ * The note's `type`, which the theme groups by.
55
+ */
56
+ type: string;
57
+ };
58
+ /**
59
+ * The `related` block one page publishes.
60
+ */
61
+ export type Related = {
62
+ /**
63
+ * Pages that link to this one.
64
+ */
65
+ backlinks: RelatedEntry[];
66
+ /**
67
+ * Pages this one links to.
68
+ */
69
+ mentions: RelatedEntry[];
70
+ };
@@ -65,14 +65,27 @@ export function collectHomepages(contentBase: string, ctx: object): {
65
65
  * states no `url` — the home kind has none to state — and the mount below it
66
66
  * holds pages and nothing else, so this is the only `_index.md` in the tree.
67
67
  *
68
+ * **Verbatim, but not unconnected.** The homepage is a page like any other on
69
+ * both sides of the link graph: a content page reaches it through
70
+ * `[[homepage-root|Text]]`, and its own markdown links name content pages. So
71
+ * it carries the same `related` block every content page does — handed in by
72
+ * the caller, since the graph is read off the content render and homepage-only
73
+ * mode has none.
74
+ *
68
75
  * @param {string} outRoot - The package's site root — the content mount's
69
76
  * root, `build/hugo/content`, one level above the mount itself.
70
77
  * @param {readonly object[]} pages - From {@link collectHomepages}.
71
78
  * @param {object} config - The resolved configuration, for the package name and
72
79
  * the default title.
80
+ * @param {object} [options] - Options.
81
+ * @param {import("./related-pages.mjs").Related} [options.related] - The
82
+ * homepage's backlinks and mentions. Absent where nothing connects to it,
83
+ * and in homepage-only mode, where no link resolves.
73
84
  * @returns {number} How many pages were written.
74
85
  */
75
- export function writeHomepages(outRoot: string, pages: readonly object[], config: object): number;
86
+ export function writeHomepages(outRoot: string, pages: readonly object[], config: object, { related }?: {
87
+ related?: import("./related-pages.mjs").Related | undefined;
88
+ }): number;
76
89
  /**
77
90
  * The integrity gates a site build runs before it writes anything.
78
91
  *
@@ -159,6 +172,12 @@ export function tableUniverse(pages: object[]): Map<string, object[]>;
159
172
  * self-describing and makes sweeping the field out of a content tree
160
173
  * output-preserving for a site as it already is for the packs.
161
174
  *
175
+ * **`related` is derived, never authored.** What links to a page is a fact
176
+ * about every other page, known only once the whole tree has resolved — see
177
+ * {@link module:engine/related-pages} — so an authored value is dropped the
178
+ * way `aliases` is, and {@link renderPages} writes the derived block once it
179
+ * holds the graph.
180
+ *
162
181
  * @param {object} page - The page.
163
182
  * @param {object} options
164
183
  * @param {(data: object, page: object) => void} [options.decorate] - Called
@@ -210,10 +229,23 @@ export function pageDestination(page: object): string;
210
229
  * touches, so the repository's own rewrite runs before the shared one
211
230
  * rather than replacing it.
212
231
  *
232
+ * **Every page is rendered before any is written.** What links *to* a page is
233
+ * known only once every other page has resolved, and it is written into the
234
+ * page's own front matter, so the render is one pass over the tree and the
235
+ * write is a second — with the link graph read off the first, at the one
236
+ * point the resolver answers, and inverted once between them. Nothing is
237
+ * resolved twice.
238
+ *
213
239
  * @param {object[]} pages - Every page.
214
- * @param {object} options - Everything the render needs.
240
+ * @param {object} options - Everything the render needs. `homepages` is the
241
+ * homepage as the index knows it — `{ fm, body, url, title }` — which takes
242
+ * no part in the render and every part in the graph: a content page links
243
+ * it by wikilink, and its own markdown links name content pages.
215
244
  * @returns {{written: number, byKind: Record<string, number>, tableErrors: object[],
216
- * wikiErrors: object[], imageErrors: object[]}}
245
+ * wikiErrors: object[], imageErrors: object[],
246
+ * related: Map<string, import("./related-pages.mjs").Related>}} `related`
247
+ * is keyed by page URL, and holds the homepage's block beside every content
248
+ * page's, so the caller can write it on the page this render does not.
217
249
  */
218
250
  export function renderPages(pages: object[], options: object): {
219
251
  written: number;
@@ -221,6 +253,7 @@ export function renderPages(pages: object[], options: object): {
221
253
  tableErrors: object[];
222
254
  wikiErrors: object[];
223
255
  imageErrors: object[];
256
+ related: Map<string, import("./related-pages.mjs").Related>;
224
257
  };
225
258
  /**
226
259
  * Resolves `site.pass` to its bundle.
@@ -139,7 +139,7 @@ export const HUGO_CONTENT: "build/hugo/content";
139
139
  * The directory that is deployed, relative to the repository root.
140
140
  *
141
141
  * Hugo renders into `<DEPLOY_ROOT>/<contentPackage>/`; `package-build
142
- * site-root` writes `_headers` and `_redirects` beside it.
142
+ * site-root` writes `_headers` beside it and the search index inside it.
143
143
  */
144
144
  export const DEPLOY_ROOT: "build/site";
145
145
  /** The npm package the shared theme arrives as. */
@@ -41,6 +41,9 @@ export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex, no
41
41
  * resolvers that distinguish a foreign hit from a local one.
42
42
  * @param {object} [options.assets] - The address space an `![[…]]` embed
43
43
  * resolves against, shaped as every asset resolver reads one.
44
+ * @param {object[]} [options.resolved] - Collector the resolver appends every
45
+ * resolved target's index entry to — the page's outbound edges, read off
46
+ * the one pass that resolves them.
44
47
  * @returns {object} The resolver context.
45
48
  *
46
49
  * There is deliberately **no `manifestsComplete`**. It used to let a resolver
@@ -49,13 +52,14 @@ export function buildSiteIndex(entries: readonly SiteEntry[], { foreignIndex, no
49
52
  * checker never had it and one authored link must not get two verdicts. A
50
53
  * caller still passing it is ignored rather than obeyed.
51
54
  */
52
- export function wikiContext(built: SiteIndex, { src, file, type, errors, foreignIndex, assets }: {
55
+ export function wikiContext(built: SiteIndex, { src, file, type, errors, foreignIndex, assets, resolved }: {
53
56
  src: string;
54
57
  type?: string | null | undefined;
55
58
  errors: object[];
56
59
  file?: string | undefined;
57
60
  foreignIndex?: Map<string, object> | undefined;
58
61
  assets?: object | undefined;
62
+ resolved?: object[] | undefined;
59
63
  }): object;
60
64
  /**
61
65
  * Resolve one infobox reference against a site index.
@@ -27,29 +27,67 @@ export function noindexHeaders(): string[];
27
27
  * @returns {string} The file's contents.
28
28
  */
29
29
  export function headers(): string;
30
+ export function indexSite(where: {
31
+ site: string;
32
+ output: string;
33
+ }): Promise<{
34
+ pages: number;
35
+ }>;
30
36
  /**
31
- * Write `_headers` beside the rendered site, and remove any `_redirects`.
37
+ * Write `_headers` beside the rendered site, remove any `_redirects`, and
38
+ * index the site for search.
32
39
  *
33
40
  * The removal is part of owning the root: a `_redirects` this build did not
34
41
  * write is one an earlier build left, and Cloudflare Pages applies whatever
35
42
  * sits there. Left in place it would redirect the prefix root — the homepage —
36
- * to an address nothing publishes.
43
+ * to an address nothing publishes. The same holds for a `pagefind/` left by an
44
+ * earlier build when `search` is off: deployed, it is a working search over
45
+ * pages that may no longer exist, so it goes too.
46
+ *
47
+ * `_headers` is written before the index is built, so a failed index leaves
48
+ * the root files as they would be — the failure is the indexer's, reported as
49
+ * one finding naming the rendered site, and nothing else about the deployment
50
+ * is in doubt.
37
51
  *
38
52
  * @param {object} options - Options.
39
53
  * @param {string} options.pkg - The content package name, which is also the
40
54
  * directory Hugo rendered into.
41
55
  * @param {string} options.out - The directory that is deployed.
42
- * @returns {{files: string[]}} The files written.
56
+ * @param {boolean} [options.search] - Whether to build the search index;
57
+ * `site.search` in the configuration. Default `true`.
58
+ * @param {SiteIndexer} [options.indexer] - What builds it. Default
59
+ * {@link indexSite}.
60
+ * @returns {Promise<{files: string[], search: {dir: string, pages: number}|null}>}
61
+ * The files written, and the index — where it is and how many pages it
62
+ * holds — or `null` when none was built.
43
63
  * @throws {Error} When no rendered site is there, which means the site build
44
64
  * has not run and writing root files would publish a deployment with nothing
45
- * under the prefix.
65
+ * under the prefix; or, located at the rendered site, when the indexer
66
+ * fails.
46
67
  */
47
- export function writeSiteRoot({ pkg, out }: {
68
+ export function writeSiteRoot({ pkg, out, search, indexer }: {
48
69
  pkg: string;
49
70
  out: string;
50
- }): {
71
+ search?: boolean | undefined;
72
+ indexer?: SiteIndexer | undefined;
73
+ }): Promise<{
51
74
  files: string[];
52
- };
75
+ search: {
76
+ dir: string;
77
+ pages: number;
78
+ } | null;
79
+ }>;
80
+ /**
81
+ * The directory the search index is written to, inside the rendered site.
82
+ *
83
+ * Pagefind's own default, and the one its browser bundle derives the site's
84
+ * base URL from: `pagefind.js` at `/<package>/pagefind/pagefind.js` means
85
+ * every result's `url` is joined onto `/<package>/`, so nothing about the
86
+ * prefix is configured on either side.
87
+ *
88
+ * @type {string}
89
+ */
90
+ export const SEARCH_DIR: string;
53
91
  /**
54
92
  * The namespace the routing layer derives a package's origin in.
55
93
  *
@@ -60,3 +98,17 @@ export function writeSiteRoot({ pkg, out }: {
60
98
  * @type {string}
61
99
  */
62
100
  export const ORIGIN_SUFFIX: string;
101
+ /**
102
+ * What runs the search indexer over a rendered site.
103
+ *
104
+ * {@link indexSite} is the one the command uses; a test hands
105
+ * {@link writeSiteRoot} a stand-in, so the step's own contract — what it is
106
+ * given, when it runs, how its failure is reported — is checked without a
107
+ * binary.
108
+ */
109
+ export type SiteIndexer = (where: {
110
+ site: string;
111
+ output: string;
112
+ }) => Promise<{
113
+ pages: number;
114
+ }>;
@@ -76,9 +76,17 @@ export function frontmatterWikilinks(fm: unknown): Array<{
76
76
  * {@link module:engine/content-images.renderImageFigures} turns into a figure
77
77
  * for the website, and what the book reads its staging list out of.
78
78
  *
79
+ * ## Every hit is recorded, so the page's edges come out of one pass
80
+ *
81
+ * A caller that hands in `ctx.resolved` is given every index entry a link on
82
+ * the page resolved to, in body order, repeats included. That is the link
83
+ * graph, read at the one point where it exists: the build never has to walk a
84
+ * page a second time to learn what it links to. Nothing is recorded for a
85
+ * same-page anchor or for a target that resolved nowhere.
86
+ *
79
87
  * @param {string} body - The markdown body.
80
88
  * @param {object} ctx - `{ index, assets, collide, contentTypes,
81
- * packages, noIndexPackages, foreign, type, errors, src, file }`.
89
+ * packages, noIndexPackages, foreign, type, errors, src, file, resolved }`.
82
90
  * `packages` is every package an address may name, without which the leading
83
91
  * package segment of a canonical address reads as an unknown type;
84
92
  * `noIndexPackages` is every package declared `contentIndex: false`, so a
@@ -87,7 +95,8 @@ export function frontmatterWikilinks(fm: unknown): Array<{
87
95
  * is the cross-package manifest index; `assets` is the address space an embed
88
96
  * resolves against. `src` is the page's display
89
97
  * path and `file` the source file a diagnostic should name — absent, `src`
90
- * stands in.
98
+ * stands in. `resolved`, when supplied, is the array every resolved
99
+ * target's index entry is appended to.
91
100
  * @returns {string} The body with embeds and wikilinks rewritten.
92
101
  */
93
102
  export function resolveWebWikilinks(body: string, ctx: object): string;