@heroiclands/package-build 22.3.1 → 22.4.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.
@@ -15,12 +15,16 @@
15
15
  * inside the prefix is published as a text file and never applied. Hugo owns
16
16
  * everything under the prefix; this owns what sits beside it.
17
17
  *
18
- * **One implementation, because it is one policy.** What is indexable, where
19
- * the prefix root sends a reader, and how long that answer is cached are
20
- * decisions about the hosting rather than about any one package. Held in each
21
- * consumer they are the same file with one constant changed, which is a file
22
- * that drifts — and the drift is invisible, because nobody reads all of the
23
- * copies at once.
18
+ * **One file, `_headers`.** The prefix root is the homepage the site build
19
+ * writes it as the mount's `_index.md` so nothing redirects, and no
20
+ * `_redirects` is written. One left beside the site by an earlier build is
21
+ * removed rather than left to send every reader somewhere nothing publishes.
22
+ *
23
+ * **One implementation, because it is one policy.** What is indexable is a
24
+ * decision about the hosting rather than about any one package. Held in each
25
+ * consumer it is the same file with one constant changed, which is a file that
26
+ * drifts — and the drift is invisible, because nobody reads all of the copies
27
+ * at once.
24
28
  *
25
29
  * @module
26
30
  */
@@ -39,19 +43,6 @@ import path from "node:path";
39
43
  */
40
44
  export const ORIGIN_SUFFIX = "pkg.heroiclands.org";
41
45
 
42
- /**
43
- * Where a package's landing is served, now that it is an addressed page.
44
- *
45
- * The site build emits the homepage at its own address rather than as the
46
- * site root's `_index.md`, so the prefix root is a redirect to it.
47
- *
48
- * @param {string} pkg - The content package name.
49
- * @returns {string} The landing's path.
50
- */
51
- export function landingPath(pkg) {
52
- return `/${pkg}/homepage-root/`;
53
- }
54
-
55
46
  /**
56
47
  * Suppress indexing of every address a deployment answers on but nobody
57
48
  * advertises.
@@ -85,54 +76,25 @@ export function noindexHeaders() {
85
76
  }
86
77
 
87
78
  /**
88
- * The lifetime pinned on the prefix-root redirect, and why it is pinned.
79
+ * The `_headers` file's contents: the `noindex` rules, and nothing else.
89
80
  *
90
- * Cloudflare Pages sets no `Cache-Control` on a redirect it generates those
91
- * responses carry `location` and nothing else and a 301 with no lifetime is
92
- * cached by a browser indefinitely, on the most-linked URL there is. An hour
93
- * keeps the 301's canonical signal without the permanence.
81
+ * No `Cache-Control` is pinned on the prefix root. It is the homepage, and a
82
+ * lifetime on it would hold a stale copy at the most-linked address after a
83
+ * deploy; Pages' own defaults for a page apply.
94
84
  *
95
- * @param {string} pkg - The content package name.
96
- * @returns {string[]} The header block's lines.
97
- */
98
- export function cacheHeaders(pkg) {
99
- return [
100
- `/${pkg}/`,
101
- " Cache-Control: max-age=3600",
102
- "",
103
- `/${pkg}`,
104
- " Cache-Control: max-age=3600",
105
- "",
106
- ];
107
- }
108
-
109
- /**
110
- * Both forms of the prefix root, because Pages matches the raw path.
111
- *
112
- * Redirect matching runs before any trailing-slash or `index.html` handling, so
113
- * `/<pkg>` and `/<pkg>/` are distinct keys and a rule on one does not catch the
114
- * other.
115
- *
116
- * @param {string} pkg - The content package name.
117
- * @returns {string} The `_redirects` file's contents.
118
- */
119
- export function redirects(pkg) {
120
- const to = landingPath(pkg);
121
- return [`/${pkg}/ ${to} 301`, `/${pkg} ${to} 301`, ""].join("\n");
122
- }
123
-
124
- /**
125
- * The `_headers` file's contents.
126
- *
127
- * @param {string} pkg - The content package name.
128
85
  * @returns {string} The file's contents.
129
86
  */
130
- export function headers(pkg) {
131
- return [...noindexHeaders(), ...cacheHeaders(pkg)].join("\n");
87
+ export function headers() {
88
+ return noindexHeaders().join("\n");
132
89
  }
133
90
 
134
91
  /**
135
- * Write `_headers` and `_redirects` beside the rendered site.
92
+ * Write `_headers` beside the rendered site, and remove any `_redirects`.
93
+ *
94
+ * The removal is part of owning the root: a `_redirects` this build did not
95
+ * write is one an earlier build left, and Cloudflare Pages applies whatever
96
+ * sits there. Left in place it would redirect the prefix root — the homepage —
97
+ * to an address nothing publishes.
136
98
  *
137
99
  * @param {object} options - Options.
138
100
  * @param {string} options.pkg - The content package name, which is also the
@@ -152,14 +114,8 @@ export function writeSiteRoot({ pkg, out }) {
152
114
  );
153
115
  }
154
116
 
155
- const written = [];
156
- for (const [name, body] of [
157
- ["_headers", headers(pkg)],
158
- ["_redirects", redirects(pkg)],
159
- ]) {
160
- const file = path.join(root, name);
161
- fs.writeFileSync(file, body);
162
- written.push(file);
163
- }
164
- return { files: written };
117
+ const file = path.join(root, "_headers");
118
+ fs.writeFileSync(file, headers());
119
+ fs.rmSync(path.join(root, "_redirects"), { force: true });
120
+ return { files: [file] };
165
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.3.1",
3
+ "version": "22.4.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",
@@ -133,10 +133,9 @@ export const DEFAULT_ADDRESS_SCHEME: Readonly<{
133
133
  * and the default.
134
134
  *
135
135
  * - `homepage` — the authored homepage, and **no other page**. The content tree
136
- * is not walked for pages, `site.sections` / `site.landing` emit nothing,
137
- * and nothing serves a page for its addresses.
138
- * - `content` — the homepage *plus* every page the content tree publishes: the
139
- * knowledgebase and the section landings.
136
+ * is not walked for pages, and nothing serves a page for its addresses.
137
+ * - `content` the homepage *plus* every page the content tree publishes, one
138
+ * per note.
140
139
  *
141
140
  * **Homepage-only is a first-class mode, not an accommodation.**
142
141
  * `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
@@ -200,10 +199,9 @@ export const DERIVED_HUGO_KEYS: Readonly<Record<string, string>>;
200
199
  * and the default.
201
200
  *
202
201
  * - `homepage` — the authored homepage, and **no other page**. The content tree
203
- * is not walked for pages, `site.sections` / `site.landing` emit nothing,
204
- * and nothing serves a page for its addresses.
205
- * - `content` — the homepage *plus* every page the content tree publishes: the
206
- * knowledgebase and the section landings.
202
+ * is not walked for pages, and nothing serves a page for its addresses.
203
+ * - `content` the homepage *plus* every page the content tree publishes, one
204
+ * per note.
207
205
  *
208
206
  * **Homepage-only is a first-class mode, not an accommodation.**
209
207
  * `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
@@ -79,41 +79,30 @@ export function buildLinkIndex(contentBase: string, { config, skipDirectories, s
79
79
  * not and cannot: it is published *verbatim* by every publishing mode, including
80
80
  * the homepage-only mode two fan-licensed packages ship under, where the content
81
81
  * tree is never walked and there is no index for a wikilink to resolve against.
82
- * So a landing addresses the web the way the web does — markdown links and
83
- * `url:` fields — and nothing was looking at those. SoHL's landing pointed at
84
- * `kb/creature/` and `kb/character/` from the day those types merged into
85
- * `being`: two 404s on the package's front page, through every build.
82
+ * So a homepage addresses the web the way the web does — markdown links in its
83
+ * body — and this is what looks at those. A dead link on the page a reader
84
+ * arrives at is the one nothing else would report.
86
85
  *
87
86
  * **What is checkable, stated plainly.** Only an address into this site is, and
88
87
  * only against facts this build already holds:
89
88
  *
90
89
  * - A **retired content type** in the path. The engine knows the retired names
91
- * and what replaced it, so this is a fact rather than a guess — and it is
92
- * exactly the SoHL defect.
90
+ * and what replaced it, so this is a fact rather than a guess.
93
91
  * - A **hardcoded absolute URL** into this package's own prefix, or into one a
94
- * a fetched index names. Every one of them has a better form to write, which
92
+ * fetched index names. Every one of them has a better form to write, which
95
93
  * is why every one is reported — including a bare `/<package>/`, which names
96
- * another package's landing.
97
- *
98
- * That last case was exempt until the better form was identified, on the
99
- * reasoning that a landing is in no link manifest so nothing could resolve it.
100
- * True, and beside the point: it does not need resolving. A landing's address
101
- * *is* its package prefix, so `/<package>/` is the absolute URL with the host
102
- * struck off — host-free, emitted verbatim, and needing no index, which is
103
- * what lets it hold in homepage-only mode where the tree is never walked. The
104
- * form was already accepted here; nothing had ever named it as the one to use.
105
- * - A **root-relative `url:`**, which the theme's `relURL` prefixes a second
106
- * time. `href:` means "already resolved, use verbatim", so the same leading
107
- * slash is correct there and is not reported.
94
+ * another package's front page. A front page's address *is* its package
95
+ * prefix, so `/<package>/` is the absolute URL with the host struck off —
96
+ * host-free, emitted verbatim, and needing no index, which is what lets it
97
+ * hold in homepage-only mode where the tree is never walked.
108
98
  * - A **wikilink**, which nothing on this page will ever resolve.
109
99
  *
110
100
  * **What is not checkable, and is not attempted.** Whether an external URL
111
101
  * answers — there is no network at build time, and a build must not fail because
112
102
  * a third party is down. And whether a live in-site address names a page that
113
- * exists: several of the surfaces a landing routes to are produced by other
114
- * tools entirely (generated API documentation, hand-authored Hugo sections), so
115
- * this build does not hold the set of published pages and would report a working
116
- * link as dead.
103
+ * exists: several of the surfaces a homepage routes to are produced by other
104
+ * tools entirely (generated API documentation, say), so this build does not
105
+ * hold the set of published pages and would report a working link as dead.
117
106
  *
118
107
  * @param {ReturnType<typeof buildLinkIndex>} index - The built index.
119
108
  * @returns {Array<{note: object, field: string, url: string, text: string,
@@ -184,7 +184,7 @@ export function positionOfLiteral(text: string, needle: string, occurrence?: num
184
184
  *
185
185
  * **`key: true` addresses the declaration rather than the value.** A finding
186
186
  * about a *value* — this pack name is not in `packs[]` — belongs on the value,
187
- * which is the default. A finding that names a **field** — `\`site.sections.x\`
187
+ * which is the default. A finding that names a **field** — `\`site.notfound.x\`
188
188
  * is not a recognized option` — sends the reader to look for that field, so the
189
189
  * position should be the field's own, and in a flow mapping
190
190
  * (`{ title: X, banner: Y }`) the two are different columns on one line. The
@@ -210,7 +210,7 @@ export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string |
210
210
  * The YAML key path a **dotted field path** addresses.
211
211
  *
212
212
  * Configuration checks report the offending key as the path a reader would
213
- * write it — `packs[1].name`, `site.sections.affliction.title` — because that
213
+ * write it — `packs[1].name`, `site.notfound.links[0].url` — because that
214
214
  * is what the message has to say. {@link positionOfYamlPath} addresses a node
215
215
  * by segments instead, so this is the one translation between them: `.`
216
216
  * separates map keys, and a bracketed suffix is a sequence index.
@@ -1,18 +1,3 @@
1
- /**
2
- * The file a homepage is written to, relative to the package's site root.
3
- *
4
- * Its **address**, flat at the package root, and stated in the page's own `url`
5
- * — the same separation every other page has, where the directory
6
- * decides the Hugo section and the front matter decides the URL. Flat rather
7
- * than inside a `homepage/` section directory, because a homepage is not one of
8
- * a kind: a section holding exactly one page would publish a landing at
9
- * `/<package>/kb/homepage/` that nothing links to and nobody wrote.
10
- *
11
- * @param {object} fm - Parsed frontmatter.
12
- * @returns {string} The destination filename, e.g. `homepage-root.md`.
13
- * @throws {Error} When the note declares no shortcode, and so has no address.
14
- */
15
- export function homepageDestination(fm: object): string;
16
1
  /**
17
2
  * Whether a note's frontmatter declares the homepage type.
18
3
  *
@@ -24,7 +9,7 @@ export function isHomepage(fm: object | null | undefined): boolean;
24
9
  * What the address rule says about one note's top-level fields.
25
10
  *
26
11
  * Two statements about the same thing, so they are made together: the field a
27
- * homepage **owes** and the field it may **not** write.
12
+ * homepage **owes** and the fields it may **not** write.
28
13
  *
29
14
  * The missing `shortcode` comes first, and is located at `type:` rather than at
30
15
  * a key that is not there — the `homepage` value is what makes the field
@@ -34,8 +19,9 @@ export function isHomepage(fm: object | null | undefined): boolean;
34
19
  * diagnostic per finding walks down the file.
35
20
  *
36
21
  * Presence is the whole test for a refused field, and absence-or-blank for the
37
- * required one: `shortcode:` authored empty is no address, and a value cannot
38
- * make `id` mean something on a page that compiles to no document.
22
+ * required one: `shortcode:` authored empty is no address, and no value can
23
+ * make `id` or `landing` mean something on a page that compiles to no document
24
+ * and renders as its body.
39
25
  *
40
26
  * Each finding carries the `locator` key to position it at, because the two
41
27
  * things that would resolve one — the raw note text and the position helper —
@@ -79,13 +65,11 @@ export function checkHomepageAddressFields(fm: object | null | undefined, { isAu
79
65
  * this exists to prevent, and it is silent — the site build reports `wrote 0
80
66
  * homepage(s)` and exits 0.
81
67
  * - _Two_ and it serves a page nobody chose. **This is a cardinality rule, and
82
- * only that.** A homepage is written at its own address, so two of them
83
- * publish two pages and collide over nothing; the
84
- * duplicate-address check catches only the pair that happen to share a
85
- * shortcode, and says nothing at all about a `homepage-root` beside a
86
- * `homepage-front`. Which of the two the redirect at `/<package>/` should
87
- * name is a question nothing here can answer, and both being reachable is
88
- * not an answer to it.
68
+ * only that.** Both are written to the mount's `_index.md`, so the second
69
+ * silently overwrites the first; the duplicate-address check catches only
70
+ * the pair that happen to share a shortcode, and says nothing at all about
71
+ * a `homepage-root` beside a `homepage-front`. Which of the two should be
72
+ * the front page is a question nothing here can answer.
89
73
  *
90
74
  * Neither has a safe default, so neither is a warning. A warning is the right
91
75
  * severity for something a build can proceed past correctly, and a build that
@@ -149,24 +133,15 @@ export function homepageTitle(fm: object | null | undefined, config: object): st
149
133
  /**
150
134
  * The frontmatter a homepage publishes with.
151
135
  *
152
- * The note's own, plus the derived values every emitted page carries: the
153
- * resolved `title`, the package the build derived — no note declares one
136
+ * The note's own, plus the two derived values every emitted page carries: the
137
+ * resolved `title`, and the package the build derived — no note declares one
154
138
  * (`package:` is retired) and the theme's breadcrumb partial reads
155
- * `.Params.package` — and its **address**.
156
- *
157
- * The address is stated as `url` for the same reason every other page states
158
- * one: Hugo publishes a page where its file sits unless told otherwise,
159
- * and a homepage's file sits at the package's site root. `slug` is written
160
- * beside it because it is the last segment of that address and Hugo's own key
161
- * for one; it decides nothing while `url` is present, but a page carrying only
162
- * `url` would report a slug Hugo had inferred from the filename.
163
- *
164
- * **Site-root relative, and so carrying no package base**, exactly as
165
- * `pageFrontmatter` states a content page's: Hugo resolves a `url`
166
- * against `baseURL`, whose path is already where the package is served, so a
167
- * stated base was written twice and published the landing at
168
- * `/<package>/<package>/homepage-root/`. Where the package is served is what
169
- * every *href* is composed from and it reaches this page's address not at all.
139
+ * `.Params.package`.
140
+ *
141
+ * **No `url` and no `slug`.** Hugo publishes the `home` kind at `baseURL`,
142
+ * whose path is already where the package is served, so the page has no
143
+ * address to state; a content page states one because its file sits under the
144
+ * mount and its address does not.
170
145
  *
171
146
  * An authored `aliases` is dropped for the same reason it is on every other
172
147
  * page: Hugo reads it as URL redirects, so passing it through would publish a
@@ -178,43 +153,33 @@ export function homepageTitle(fm: object | null | undefined, config: object): st
178
153
  * @param {string} options.contentPackage - The package this build publishes.
179
154
  * @param {string} options.title - The resolved title.
180
155
  * @returns {object} The frontmatter to write.
181
- * @throws {Error} When the note declares no shortcode, and so has no address.
182
156
  */
183
157
  export function homepageFrontmatter(fm: object, { contentPackage, title }: {
184
158
  contentPackage: string;
185
159
  title: string;
186
160
  }): object;
187
161
  /**
188
- * Every address a homepage carries, wherever it is written.
189
- *
190
- * **Both halves of the page are in scope, and that is the finding rather than
191
- * the assumption.** Of the six homepages authored today, four carry every link
192
- * in the body as ordinary markdown and two carry them in `landing:` — and the
193
- * one whose dead links prompted the check has an *empty body*, so a body-only
194
- * reading would have found nothing at all on it. A dead link in a card is
195
- * exactly as broken as one in a paragraph.
162
+ * Every address a homepage carries: the markdown links in its body.
196
163
  *
197
- * Three shapes are gathered, and the caller needs to tell them apart because
198
- * the rules differ:
164
+ * Nowhere else: the frontmatter holds no address, because a homepage is a
165
+ * page with a body and the card block that once carried links is refused.
166
+ * Top-level `title` and `description` are not walked — they are set as text,
167
+ * never rendered as markdown — and `banner:` is not an address: it is an image
168
+ * path resolved through the CDN base, and `banner: none` is a sentinel rather
169
+ * than a target.
199
170
  *
200
- * - **`url`** package-relative, resolved against the site by the theme.
201
- * - **`href`** already resolved, used verbatim.
202
- * - **prose and body markdown links** — emitted as written and resolved by the
203
- * browser against the landing's own address, which *is* the package root, so
204
- * a relative one means the same thing a `url` does.
171
+ * A body link is emitted as written and resolved by the browser against the
172
+ * homepage's own address, which *is* the package root, so a package-relative
173
+ * one (`kb/rules/`) lands where a reader expects.
205
174
  *
206
- * `banner:` is not an address: it is an image path resolved through the CDN
207
- * base, and `banner: none` is a sentinel rather than a target. Top-level
208
- * `title` and `description` are not walked either — they are set as text, never
209
- * rendered as markdown.
175
+ * Links inside code are ignored, so an example in a fenced block is not
176
+ * reported as a dead address.
210
177
  *
211
- * @param {object|null|undefined} fm - The note's frontmatter.
212
178
  * @param {string} [body] - The note's markdown body.
213
179
  * @returns {Array<{field: string, url: string, kind: string}>} Every address,
214
- * frontmatter first and then the body, each with the dotted path it was
215
- * written at.
180
+ * in body order, each recorded at `field: "body"` with `kind: "body"`.
216
181
  */
217
- export function homepageAddresses(fm: object | null | undefined, body?: string): Array<{
182
+ export function homepageAddresses(body?: string): Array<{
218
183
  field: string;
219
184
  url: string;
220
185
  kind: string;
@@ -250,17 +215,28 @@ export const HOMEPAGE_FIELDS: readonly import("./field-spec.mjs").FieldSpec[];
250
215
  */
251
216
  export const HOMEPAGE_SHORTCODE: string;
252
217
  /**
253
- * The top-level field a homepage refuses, and what it would decide.
218
+ * The file a homepage is written to, relative to the package's site root.
254
219
  *
255
- * **One field rather than three.** `name` and `shortcode` are
256
- * refused because a page's URL derived from `name.full` while a homepage's
257
- * destination was fixed, so the address a `shortcode` computed named a page the
258
- * site build never wrote. A page's URL is its address now and a homepage
259
- * publishes at its own, so both fields decide exactly what they decide
260
- * everywhere else and are permitted.
220
+ * The mount's own `_index.md`: Hugo renders it as the `home` kind, at
221
+ * `baseURL` which is `/<package>/`, the package's own address. One fixed
222
+ * destination rather than one derived from the note's address, because the
223
+ * homepage's address *is* the package root; the note's `shortcode` names the
224
+ * page in links and decides no file.
261
225
  *
262
- * `id` is untouched by that, and stays: it is the Foundry document id a
263
- * compendium UUID is built from, and a homepage compiles into no document.
226
+ * @type {string}
227
+ */
228
+ export const HOMEPAGE_DESTINATION: string;
229
+ /**
230
+ * The top-level fields a homepage refuses, and what each would decide.
231
+ *
232
+ * `id` is the Foundry document id a compendium UUID is built from, and a
233
+ * homepage compiles into no document. `landing` is a card block; the homepage
234
+ * is a page with a body, rendered as one, so nothing reads it — and an index
235
+ * of what the package publishes is a `doc` note carrying a content table,
236
+ * authored where every other page is.
237
+ *
238
+ * `name` and `shortcode` are permitted: the shortcode is what a link is
239
+ * written with, and `name` titles the page like every other note's.
264
240
  *
265
241
  * **A named class, not an allow-list, and that boundary is the decision.** A
266
242
  * homepage's frontmatter is *emitted into the published page*
@@ -276,19 +252,3 @@ export const HOMEPAGE_SHORTCODE: string;
276
252
  * @type {ReadonlyMap<string, string>}
277
253
  */
278
254
  export const HOMEPAGE_REFUSED_FIELDS: ReadonlyMap<string, string>;
279
- /**
280
- * The two frontmatter keys that hold an address, and what each one means.
281
- *
282
- * They are **not** interchangeable, and a check that treated them as one would
283
- * be wrong about both. The theme resolves a `url` against the site with
284
- * `relURL`, so a package writes `kb/rules/` and is served `/sohl/kb/rules/`
285
- * without ever naming its own prefix. An `href` is an address that is *already*
286
- * resolved and is used verbatim — which is what `cards.source: sections` fills
287
- * in, since a section's permalink already carries the prefix.
288
- *
289
- * So a leading `/` is a defect in a `url` (it is prefixed a second time) and
290
- * correct in an `href`.
291
- *
292
- * @type {ReadonlySet<string>}
293
- */
294
- export const HOMEPAGE_ADDRESS_KEYS: ReadonlySet<string>;
@@ -50,18 +50,6 @@ export function exclusiveTagGroups(type: string, groups?: object): {
50
50
  * @returns {boolean} Whether the note carries it.
51
51
  */
52
52
  export function hasTag(fm: object | null | undefined, tag: string): boolean;
53
- /**
54
- * Whether a note carries any `tags:` at all, however authored.
55
- *
56
- * The one question the site build asks of tags in aggregate — whether the
57
- * tree publishes taxonomy pages — rather than about a particular tag. Reads
58
- * `tags` and `tag` exactly as {@link hasTag} does, and treats an empty list
59
- * or a blank string as carrying none.
60
- *
61
- * @param {object|null|undefined} fm - Parsed frontmatter.
62
- * @returns {boolean} Whether the note carries at least one tag.
63
- */
64
- export function hasAnyTag(fm: object | null | undefined): boolean;
65
53
  /**
66
54
  * Whether a note is tagged as an unfinished **draft**.
67
55
  *