@heroiclands/package-build 13.0.0 → 15.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 +361 -0
- package/CONTENT.md +193 -43
- package/MIGRATING.md +196 -5
- package/content-config.mjs +99 -61
- package/docs/content-format.md +28 -8
- package/engine/content-address.mjs +11 -19
- package/engine/field-reference.mjs +29 -0
- package/engine/field-spec.mjs +25 -0
- package/engine/frontmatter-lint.mjs +99 -32
- package/engine/helpers.mjs +38 -12
- package/engine/homepage.mjs +9 -4
- package/engine/item-registry.mjs +4 -1
- package/engine/macros.mjs +3 -1
- package/engine/manifest-emit.mjs +4 -7
- package/engine/note-vocabulary.mjs +34 -76
- package/engine/site-build.mjs +20 -14
- package/engine/system-block.mjs +29 -3
- package/package.json +1 -1
- package/sohl/actors.mjs +6 -3
- package/sohl/item-fields.mjs +6 -0
- package/sohl/items.mjs +4 -1
- package/types/content-config.d.mts +25 -35
- package/types/engine/content-address.d.mts +7 -14
- package/types/engine/field-spec.d.mts +53 -0
- package/types/engine/helpers.d.mts +34 -12
- package/types/engine/homepage.d.mts +8 -4
- package/types/engine/manifest-emit.d.mts +3 -9
- package/types/engine/note-vocabulary.d.mts +17 -47
- package/types/engine/site-build.d.mts +11 -6
|
@@ -63,6 +63,31 @@ export { setPath };
|
|
|
63
63
|
* still read, second, until #126 moves the corpus off it.
|
|
64
64
|
*
|
|
65
65
|
* Absent means the value is not authored at all — see `value`.
|
|
66
|
+
* @property {string} [topLevelMeans] - **What the note's top-level key of this
|
|
67
|
+
* name means instead** — declared only where it means something else, and
|
|
68
|
+
* stating it removes the shared top-level position from this field's
|
|
69
|
+
* resolution order (#218).
|
|
70
|
+
*
|
|
71
|
+
* A field's `name` doubles as its identity and as the shared property it
|
|
72
|
+
* draws from, which is right wherever the two levels state the same quantity
|
|
73
|
+
* — `data.weight` is the weight, whoever reads it. It is wrong wherever a
|
|
74
|
+
* spelling collides across the two vocabularies. An `affiliation` item's
|
|
75
|
+
* `system.title` is the style of address an office carries; a note's
|
|
76
|
+
* top-level `title` is the note's own heading. Nothing relates them, and
|
|
77
|
+
* before this key one silently fed the other, stringifying an authored
|
|
78
|
+
* `title: null` into fifteen documents.
|
|
79
|
+
*
|
|
80
|
+
* **The value is the reason**, not a flag with a comment beside it. A boolean
|
|
81
|
+
* would record the decision and lose the case for it, and the next person
|
|
82
|
+
* adding a field needs to know the question exists — this package's own rule
|
|
83
|
+
* that the declaration *is* the statement, never a description of one. The
|
|
84
|
+
* author-facing reference renders it, so an author reading the field table
|
|
85
|
+
* learns that the top-level key will not fill this field, and why.
|
|
86
|
+
*
|
|
87
|
+
* The exempted field is still authorable, at both of the positions that
|
|
88
|
+
* describe the *document* rather than the note: `<system>.system.<to>` and
|
|
89
|
+
* the legacy in-block `<system>.<name>`. Absent means the ordinary case —
|
|
90
|
+
* the top level is read, as the third step.
|
|
66
91
|
* @property {string} [shape] - Human-readable shape, for documentation. Comes
|
|
67
92
|
* paired with `read` from one of the coercion constants below.
|
|
68
93
|
* @property {(raw: any, ctx: {fm: object, field: FieldSpec}) => any} [read] -
|
|
@@ -170,6 +195,34 @@ export type FieldSpec = {
|
|
|
170
195
|
* Absent means the value is not authored at all — see `value`.
|
|
171
196
|
*/
|
|
172
197
|
name?: string | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* - **What the note's top-level key of this
|
|
200
|
+
* name means instead** — declared only where it means something else, and
|
|
201
|
+
* stating it removes the shared top-level position from this field's
|
|
202
|
+
* resolution order (#218).
|
|
203
|
+
*
|
|
204
|
+
* A field's `name` doubles as its identity and as the shared property it
|
|
205
|
+
* draws from, which is right wherever the two levels state the same quantity
|
|
206
|
+
* — `data.weight` is the weight, whoever reads it. It is wrong wherever a
|
|
207
|
+
* spelling collides across the two vocabularies. An `affiliation` item's
|
|
208
|
+
* `system.title` is the style of address an office carries; a note's
|
|
209
|
+
* top-level `title` is the note's own heading. Nothing relates them, and
|
|
210
|
+
* before this key one silently fed the other, stringifying an authored
|
|
211
|
+
* `title: null` into fifteen documents.
|
|
212
|
+
*
|
|
213
|
+
* **The value is the reason**, not a flag with a comment beside it. A boolean
|
|
214
|
+
* would record the decision and lose the case for it, and the next person
|
|
215
|
+
* adding a field needs to know the question exists — this package's own rule
|
|
216
|
+
* that the declaration *is* the statement, never a description of one. The
|
|
217
|
+
* author-facing reference renders it, so an author reading the field table
|
|
218
|
+
* learns that the top-level key will not fill this field, and why.
|
|
219
|
+
*
|
|
220
|
+
* The exempted field is still authorable, at both of the positions that
|
|
221
|
+
* describe the *document* rather than the note: `<system>.system.<to>` and
|
|
222
|
+
* the legacy in-block `<system>.<name>`. Absent means the ordinary case —
|
|
223
|
+
* the top level is read, as the third step.
|
|
224
|
+
*/
|
|
225
|
+
topLevelMeans?: string | undefined;
|
|
173
226
|
/**
|
|
174
227
|
* - Human-readable shape, for documentation. Comes
|
|
175
228
|
* paired with `read` from one of the coercion constants below.
|
|
@@ -117,25 +117,47 @@ export function makeFilename(name: any, id: any): string;
|
|
|
117
117
|
* asset roots — `icons/...` and `images/...` — are served from the package
|
|
118
118
|
* directory, so they are rewritten to `<assetRoot>/<path>` — `systems/sohl/assets`
|
|
119
119
|
* for this repository, `modules/<id>/assets` for a module (#1508). Any other
|
|
120
|
-
* path (already package-rooted, an absolute URL) is returned unchanged
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
120
|
+
* path (already package-rooted, an absolute URL) is returned unchanged.
|
|
121
|
+
*
|
|
122
|
+
* **Two empties, and they mean opposite things (#218).** `null` — or an absent
|
|
123
|
+
* key, which reaches here as `undefined` — means _unset_: the note names no art
|
|
124
|
+
* and the caller's default applies. `""` means _blank on purpose_: the note
|
|
125
|
+
* names no art **and wants none**, so no default may replace it. Both come back
|
|
126
|
+
* distinguishable, `null` and `""` respectively, and neither is invented from
|
|
127
|
+
* the other.
|
|
128
|
+
*
|
|
129
|
+
* This used to open `if (!raw) return ""`, which made the two one case: every
|
|
130
|
+
* caller then applied its default with `||`, so a deliberate blank was
|
|
131
|
+
* unspellable and an unset key and an empty string compiled identically. That
|
|
132
|
+
* is the convention the project already rejects for an optional "not specified"
|
|
133
|
+
* DataModel string, where `nullable, initial: null` keeps "unset" a single
|
|
134
|
+
* honest value rather than two.
|
|
135
|
+
*
|
|
136
|
+
* **`title` does not follow this rule**, and must not be made to. On a
|
|
137
|
+
* `type: affiliation` note `title` is *also* a declared item field whose default
|
|
138
|
+
* is `""` (`sohl/item-fields.mjs`), resolved from the very same shared top-level
|
|
139
|
+
* key the site emitter reads as the page title — so `title: null` stringifies
|
|
140
|
+
* into the compiled document as the literal `"null"`. One key, two destinations
|
|
141
|
+
* that disagree about what empty means; see #218.
|
|
142
|
+
*
|
|
143
|
+
* This is translation only: the default for an unset path is domain-specific
|
|
144
|
+
* (actors default differently from items, and gear differently again), so each
|
|
145
|
+
* compiler owns its own default and applies it to the result with **nullish**
|
|
146
|
+
* coalescing — `resolveImg(fm.img) ?? <default>`. Not `||`: that would collapse
|
|
147
|
+
* a deliberate blank back into the default and undo the distinction. For items
|
|
148
|
+
* that default is the art paired with the type's builder, reached through
|
|
149
|
+
* `itemArt()`, which runs the path back through this function so a registry
|
|
150
|
+
* entry and a note's `img:` are spelled the same way (#7).
|
|
130
151
|
*
|
|
131
152
|
* @param {string | null | undefined} raw - content-relative path from frontmatter.
|
|
132
153
|
* @param {{assetRoot: string}} [config] - The resolved build configuration.
|
|
133
154
|
* Defaults to this repository's.
|
|
134
|
-
* @returns {string} the Foundry-relative path
|
|
155
|
+
* @returns {string | null} the Foundry-relative path; `""` for a deliberate
|
|
156
|
+
* blank, and `null` when the note names no art at all.
|
|
135
157
|
*/
|
|
136
158
|
export function resolveImg(raw: string | null | undefined, config?: {
|
|
137
159
|
assetRoot: string;
|
|
138
|
-
}): string;
|
|
160
|
+
}): string | null;
|
|
139
161
|
/**
|
|
140
162
|
* Resolves the display name from frontmatter, preferring `name.full`,
|
|
141
163
|
* falling back to `name` (if string), then `defaultValue`.
|
|
@@ -149,6 +149,13 @@ export function homepageTitle(fm: object | null | undefined, config: object): st
|
|
|
149
149
|
* for one; it decides nothing while `url` is present, but a page carrying only
|
|
150
150
|
* `url` would report a slug Hugo had inferred from the filename.
|
|
151
151
|
*
|
|
152
|
+
* **Site-root relative, and so carrying no package base** (#217), exactly as
|
|
153
|
+
* `pageFrontmatter` states a content page's: Hugo resolves a `url`
|
|
154
|
+
* against `baseURL`, whose path is already where the package is served, so a
|
|
155
|
+
* stated base was written twice and published the landing at
|
|
156
|
+
* `/<package>/<package>/homepage-root/`. Where the package is served is what
|
|
157
|
+
* every *href* is composed from and it reaches this page's address not at all.
|
|
158
|
+
*
|
|
152
159
|
* An authored `aliases` is dropped for the same reason it is on every other
|
|
153
160
|
* page: Hugo reads it as URL redirects, so passing it through would publish a
|
|
154
161
|
* redirect stub at each one. The field is retired (#180) and refused before a
|
|
@@ -158,15 +165,12 @@ export function homepageTitle(fm: object | null | undefined, config: object): st
|
|
|
158
165
|
* @param {object} options - Options.
|
|
159
166
|
* @param {string} options.contentPackage - The package this build publishes.
|
|
160
167
|
* @param {string} options.title - The resolved title.
|
|
161
|
-
* @param {string} options.base - Where the package is served, with both
|
|
162
|
-
* slashes — `/<package>/`.
|
|
163
168
|
* @returns {object} The frontmatter to write.
|
|
164
169
|
* @throws {Error} When the note declares no shortcode, and so has no address.
|
|
165
170
|
*/
|
|
166
|
-
export function homepageFrontmatter(fm: object, { contentPackage, title
|
|
171
|
+
export function homepageFrontmatter(fm: object, { contentPackage, title }: {
|
|
167
172
|
contentPackage: string;
|
|
168
173
|
title: string;
|
|
169
|
-
base: string;
|
|
170
174
|
}): object;
|
|
171
175
|
/**
|
|
172
176
|
* Every address a homepage carries, wherever it is written.
|
|
@@ -51,8 +51,7 @@ export function entriesForNote(fm: object, name: string, address: string, body:
|
|
|
51
51
|
* exist.
|
|
52
52
|
*
|
|
53
53
|
* @param {string} contentBase - Absolute path to the content tree.
|
|
54
|
-
* @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter
|
|
55
|
-
* scheme }`.
|
|
54
|
+
* @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter }`.
|
|
56
55
|
* @returns {{entries: Array<object>, notes: number,
|
|
57
56
|
* skipped: Array<{file: string, reason: string}>}}
|
|
58
57
|
*/
|
|
@@ -65,7 +64,7 @@ export function collectManifestEntries(contentBase: string, ctx: object): {
|
|
|
65
64
|
}>;
|
|
66
65
|
};
|
|
67
66
|
/**
|
|
68
|
-
* The identities
|
|
67
|
+
* The identities an emission runs against, from configuration.
|
|
69
68
|
*
|
|
70
69
|
* Resolved in one place and passed down, rather than read at each use, so the
|
|
71
70
|
* pass itself is a pure function of its context and a test can drive it without
|
|
@@ -73,17 +72,12 @@ export function collectManifestEntries(contentBase: string, ctx: object): {
|
|
|
73
72
|
*
|
|
74
73
|
* @param {object} [config] - A resolved configuration; loaded when omitted.
|
|
75
74
|
* @returns {{contentPackage: string, foundryPackageId: string, packRouter: object,
|
|
76
|
-
*
|
|
77
|
-
* skipDirectories: readonly string[]}}
|
|
75
|
+
* web: boolean, skipDirectories: readonly string[]}}
|
|
78
76
|
*/
|
|
79
77
|
export function manifestContext(config?: object): {
|
|
80
78
|
contentPackage: string;
|
|
81
79
|
foundryPackageId: string;
|
|
82
80
|
packRouter: object;
|
|
83
|
-
scheme: {
|
|
84
|
-
prefix: string;
|
|
85
|
-
landing: string;
|
|
86
|
-
};
|
|
87
81
|
web: boolean;
|
|
88
82
|
skipDirectories: readonly string[];
|
|
89
83
|
};
|
|
@@ -36,30 +36,20 @@ export function hasTag(fm: object | null | undefined, tag: string): boolean;
|
|
|
36
36
|
*/
|
|
37
37
|
export function isDraftNote(fm: object | null | undefined): boolean;
|
|
38
38
|
/**
|
|
39
|
-
* What
|
|
40
|
-
*
|
|
41
|
-
* @param {string} type - The note's `type`.
|
|
42
|
-
* @param {string} value - The authored `subType`.
|
|
43
|
-
* @param {Readonly<Record<string, Readonly<Record<string, string>>>>} [retired]
|
|
44
|
-
* The map to read, defaulting to {@link RETIRED_SUBTYPES}.
|
|
45
|
-
* @returns {string|undefined} The current spelling, or `undefined` when the
|
|
46
|
-
* value is not a retired one — which is not the same as it being valid.
|
|
47
|
-
*/
|
|
48
|
-
export function retiredSubType(type: string, value: string, retired?: Readonly<Record<string, Readonly<Record<string, string>>>>): string | undefined;
|
|
49
|
-
/**
|
|
50
|
-
* What a note carrying a retired subType is told.
|
|
39
|
+
* What a note carrying a subType outside the address charset is told.
|
|
51
40
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
41
|
+
* **Why the charset holds for a subType, which reaches no address.** #206 said
|
|
42
|
+
* "the hyphen separates the segments of an address", and that was true of a
|
|
43
|
+
* subType when it shipped: `sectionOf` returned a `doc`'s subType, so the value
|
|
44
|
+
* was a URL path segment. #204 retired sections and it is not one now. The rule
|
|
45
|
+
* stays, on its own footing: a subType is a vocabulary term the whole toolchain
|
|
46
|
+
* keys on, and it is one closed set away from being an address segment again —
|
|
47
|
+
* so the reason to spell it in the address charset is that a charset holding
|
|
48
|
+
* for a type, a shortcode and a `contentPackage` but not for a subType is a
|
|
49
|
+
* rule nobody can state in a sentence.
|
|
54
50
|
*
|
|
55
|
-
* @
|
|
56
|
-
*
|
|
57
|
-
* @param {string} replacement - What to write instead.
|
|
58
|
-
* @returns {string} The message.
|
|
59
|
-
*/
|
|
60
|
-
export function retiredSubTypeMessage(type: string, value: string, replacement: string): string;
|
|
61
|
-
/**
|
|
62
|
-
* What a note carrying a subType outside the address charset is told.
|
|
51
|
+
* Contrast {@link typeCharsetMessage}, which keeps the address reasoning
|
|
52
|
+
* because a type genuinely is the first segment of every address.
|
|
63
53
|
*
|
|
64
54
|
* @param {string} value - The authored `subType`.
|
|
65
55
|
* @returns {string} The message.
|
|
@@ -80,6 +70,11 @@ export function typeCharsetMessage(type: string): string;
|
|
|
80
70
|
* a note's bad value is one author's mistake and belongs in a report, while a
|
|
81
71
|
* bad *declaration* would tell every author to write something unaddressable.
|
|
82
72
|
*
|
|
73
|
+
* The message states the reason **per key**, as {@link typeCharsetMessage} and
|
|
74
|
+
* {@link subTypeCharsetMessage} do: a type is an address segment, and a subType
|
|
75
|
+
* has not been one since #204 retired sections, so a single claim covering both
|
|
76
|
+
* would be half wrong (#210).
|
|
77
|
+
*
|
|
83
78
|
* @param {Readonly<Record<string, TypeVocabulary>>} vocabulary - The registry.
|
|
84
79
|
* @param {string} [where] - What declares it, for the message.
|
|
85
80
|
* @throws {Error} Naming every offending type and subType at once, rather than
|
|
@@ -288,31 +283,6 @@ export const NOTE_VOCABULARY: Readonly<{
|
|
|
288
283
|
})[];
|
|
289
284
|
}>;
|
|
290
285
|
}>;
|
|
291
|
-
/**
|
|
292
|
-
* The retired spelling of a subType a type declares → what to write now (#206).
|
|
293
|
-
*
|
|
294
|
-
* Keyed by type, because a retirement is a statement about *that type's*
|
|
295
|
-
* vocabulary: `user-guide` on a `doc` is the old spelling of `userguide`, while
|
|
296
|
-
* the same string on any other type is nothing but a charset violation, and
|
|
297
|
-
* saying "did you mean userguide" there would be a guess dressed as a fact.
|
|
298
|
-
*
|
|
299
|
-
* **Recorded here rather than left in `subTypes`** so the declared list stays
|
|
300
|
-
* the list of values a note *should* write. A retired value is accepted, not
|
|
301
|
-
* declared — the difference is exactly what makes the finding possible.
|
|
302
|
-
*
|
|
303
|
-
* **Deliberately not the shape of a type rename** ({@link
|
|
304
|
-
* import("./ids.mjs").RETIRED_TYPES}), which is an error: a retired type routes
|
|
305
|
-
* a note to the wrong pack, whereas a retired subType still compiles to the
|
|
306
|
-
* correct page. The sweep is the consumer's, and the ordering is the reverse of
|
|
307
|
-
* the usual — the acceptance ships *first*, because declaring only the new
|
|
308
|
-
* spelling while 43 `sohl` notes still author the old one would invalidate all
|
|
309
|
-
* 43 with a release they had no chance to sweep ahead of. A later change
|
|
310
|
-
* removes this map, and the old spelling then falls through to the ordinary
|
|
311
|
-
* undeclared-value error with no code left to remove.
|
|
312
|
-
*
|
|
313
|
-
* @type {Readonly<Record<string, Readonly<Record<string, string>>>>}
|
|
314
|
-
*/
|
|
315
|
-
export const RETIRED_SUBTYPES: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
316
286
|
/**
|
|
317
287
|
* One `data:` key a note type may carry.
|
|
318
288
|
*
|
|
@@ -102,14 +102,9 @@ export function collectHomepages(contentBase: string, ctx: object): {
|
|
|
102
102
|
* @param {readonly object[]} pages - From {@link collectHomepages}.
|
|
103
103
|
* @param {object} config - The resolved configuration, for the package name and
|
|
104
104
|
* the default title.
|
|
105
|
-
* @param {object} [options] - Options.
|
|
106
|
-
* @param {string} [options.base] - Where the package is served; defaults to the
|
|
107
|
-
* configured `site.base`, and to `/<contentPackage>/` below that.
|
|
108
105
|
* @returns {number} How many pages were written.
|
|
109
106
|
*/
|
|
110
|
-
export function writeHomepages(outRoot: string, pages: readonly object[], config: object
|
|
111
|
-
base?: string | undefined;
|
|
112
|
-
}): number;
|
|
107
|
+
export function writeHomepages(outRoot: string, pages: readonly object[], config: object): number;
|
|
113
108
|
/**
|
|
114
109
|
* The integrity gates a site build runs before it writes anything.
|
|
115
110
|
*
|
|
@@ -201,6 +196,16 @@ export function sectionFrontmatter(meta: object): object;
|
|
|
201
196
|
* package-wide address the link manifest records — the same address, one
|
|
202
197
|
* segment too deep. So the address is stated and the mount does not reach it.
|
|
203
198
|
*
|
|
199
|
+
* **It is stated relative to the site root, and so carries no package base**
|
|
200
|
+
* (#217). Hugo resolves a `url` against `baseURL`, whose path is already where
|
|
201
|
+
* the package is served — a consumer's Hugo site *is* its package — so writing
|
|
202
|
+
* `page.url`, which carries the base for every href this build renders, wrote
|
|
203
|
+
* that base a second time and published every content page a segment too deep
|
|
204
|
+
* (`/sohl/sohl/doc-rulesintro/`). The two are separate quantities: the page
|
|
205
|
+
* states `/<slug>/`, and everything that points *at* the page — the address
|
|
206
|
+
* index a wikilink resolves through, and the link manifest — composes
|
|
207
|
+
* `<base><slug>/`.
|
|
208
|
+
*
|
|
204
209
|
* A content page carries the package the build **derived** (#65). No note
|
|
205
210
|
* declares one — `package:` is retired (#56) — so the note's frontmatter alone
|
|
206
211
|
* would publish a page that does not say which package it belongs to. The
|