@heroiclands/package-build 4.0.0 → 6.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.
@@ -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
- * Render this package's knowledgebase/site pages.
362
+ * How much of this package reaches the web.
363
+ * See {@link SITE_MODES}.
276
364
  */
277
- site: boolean;
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?: boolean | undefined;
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. Each defaults to off.
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
  };
@@ -57,6 +57,28 @@ export class BasePackCompiler {
57
57
  * @type {boolean}
58
58
  */
59
59
  static convertsWikilinks: boolean;
60
+ /**
61
+ * The document types whose **compiled output** this pass reads.
62
+ *
63
+ * Empty for every pass that reads only the content tree. The actors pass
64
+ * is the exception: a being names its embedded items by
65
+ * `(type, shortcode)`, and it resolves them against the JSON the item
66
+ * passes wrote — so an Actor pass must run after every Item pass, and it
67
+ * says so here.
68
+ *
69
+ * The generator derives the compile order from this (#73), so the order
70
+ * `packs:` declares is presentation only — it is the manifest's `packs`
71
+ * array as well, and a consumer orders that for a reader. A pass that
72
+ * reads another's output states the dependency once, in the class that
73
+ * does the reading, instead of every consuming repository having to know
74
+ * it when writing its pack list.
75
+ *
76
+ * A consumer registering a compiler of its own declares its dependencies
77
+ * the same way; a type no pack declares is simply not waited for.
78
+ *
79
+ * @type {readonly string[]}
80
+ */
81
+ static readsPackOutputOf: readonly string[];
60
82
  /**
61
83
  * @param {object} options
62
84
  * @param {string} options.contentBase - Root of the content tree.
@@ -26,18 +26,69 @@ export function buildLinkIndex(contentBase: string, { manifestDir, skipDirectori
26
26
  manifestDir?: string | undefined;
27
27
  skipDirectories?: readonly string[] | undefined;
28
28
  }): object;
29
+ /**
30
+ * Every defect in the addresses a package homepage carries.
31
+ *
32
+ * **Why the homepage needs its own audit at all.** Every other note addresses
33
+ * the corpus with wikilinks, which {@link auditLinks} resolves. A homepage does
34
+ * not and cannot: it is published *verbatim* by every publishing mode, including
35
+ * the homepage-only mode two fan-licensed packages ship under, where the content
36
+ * tree is never walked and there is no index for a wikilink to resolve against.
37
+ * So a landing addresses the web the way the web does — markdown links and
38
+ * `url:` fields — and nothing was looking at those. SoHL's landing pointed at
39
+ * `kb/creature/` and `kb/character/` from the day those types merged into
40
+ * `being`: two 404s on the package's front page, through every build.
41
+ *
42
+ * **What is checkable, stated plainly.** Only an address into this site is, and
43
+ * only against facts this build already holds:
44
+ *
45
+ * - A **retired content type** in the path. The engine knows what used to exist
46
+ * and what replaced it, so this is a fact rather than a guess — and it is
47
+ * exactly the SoHL defect.
48
+ * - A **hardcoded absolute URL** into this package's own prefix, or into one a
49
+ * vendored manifest names. Both have a better form to write, which is why they
50
+ * are reported; a bare `/<package>/` is left alone, because a package
51
+ * homepage is in no manifest and there is nothing better to write.
52
+ * - A **root-relative `url:`**, which the theme's `relURL` prefixes a second
53
+ * time. `href:` means "already resolved, use verbatim", so the same leading
54
+ * slash is correct there and is not reported.
55
+ * - A **wikilink**, which nothing on this page will ever resolve.
56
+ *
57
+ * **What is not checkable, and is not attempted.** Whether an external URL
58
+ * answers — there is no network at build time, and a build must not fail because
59
+ * a third party is down. And whether a live in-site address names a page that
60
+ * exists: several of the surfaces a landing routes to are produced by other
61
+ * tools entirely (generated API documentation, hand-authored Hugo sections), so
62
+ * this build does not hold the set of published pages and would report a working
63
+ * link as dead.
64
+ *
65
+ * @param {ReturnType<typeof buildLinkIndex>} index - The built index.
66
+ * @returns {Array<{note: object, field: string, url: string, text: string,
67
+ * occurrence: number, message: string}>} One finding per defect, `text` and
68
+ * `occurrence` locating it in the note's raw source.
69
+ */
70
+ export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Array<{
71
+ note: object;
72
+ field: string;
73
+ url: string;
74
+ text: string;
75
+ occurrence: number;
76
+ message: string;
77
+ }>;
29
78
  /**
30
79
  * Every link in a tree that lands nowhere.
31
80
  *
32
81
  * @param {ReturnType<typeof buildLinkIndex>} index - The built index.
33
82
  * @returns {{deadAnchors: object[], deadAddresses: object[],
34
- * frontmatterLinks: object[], usedManifest: Set<string>}} The findings, and
35
- * which addresses a foreign manifest answered.
83
+ * frontmatterLinks: object[], homepageLinks: object[],
84
+ * usedManifest: Set<string>}} The findings, and which addresses a foreign
85
+ * manifest answered.
36
86
  */
37
87
  export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
38
88
  deadAnchors: object[];
39
89
  deadAddresses: object[];
40
90
  frontmatterLinks: object[];
91
+ homepageLinks: object[];
41
92
  usedManifest: Set<string>;
42
93
  };
43
94
  /**
@@ -150,3 +150,31 @@ export function positionOfLiteral(text: string, needle: string, occurrence?: num
150
150
  line?: number;
151
151
  column?: number;
152
152
  };
153
+ /**
154
+ * Where a **key or value in a YAML document** sits, addressed by its path.
155
+ *
156
+ * {@link positionOfLiteral} is the plain search, and it is the wrong tool for a
157
+ * configuration file: a pack name like `items` or `actors` appears in the
158
+ * `packs:` block, in a folder's list, in a path, and often in prose, so the
159
+ * first occurrence is routinely not the one the finding is about — which is the
160
+ * one thing the located form exists to prevent.
161
+ *
162
+ * A path resolves the exact node instead. The document is re-parsed here rather
163
+ * than threaded from the loader because the loader returns plain data: `yaml`
164
+ * discards ranges once a document is materialised, and carrying a parallel
165
+ * position tree through configuration resolution would be a second
166
+ * representation of the same file to keep in step.
167
+ *
168
+ * Every failure — unparseable text, an `.mjs` configuration, a path that
169
+ * resolves to nothing — yields `{}`, so a caller spreads the result and the
170
+ * position is dropped rather than guessed.
171
+ *
172
+ * @param {string} text - The document's contents.
173
+ * @param {ReadonlyArray<string|number>} keyPath - Path to the node: map keys as
174
+ * strings, sequence entries as numbers.
175
+ * @returns {{line?: number, column?: number}} Spreadable position fields.
176
+ */
177
+ export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string | number>): {
178
+ line?: number;
179
+ column?: number;
180
+ };
@@ -6,8 +6,9 @@
6
6
  * and an actor's embedded items may be sourced from any of them. Finding one
7
7
  * pack and stopping is how embedded-item resolution would silently miss every
8
8
  * item that landed in another. Returned in configured order, which is also the
9
- * order they compile in, so a pack later in the list cannot be read before it
10
- * is written.
9
+ * order they compile in {@link orderPassesByDependency} keeps the declared
10
+ * order among packs of one type — and every one of them is written before the
11
+ * actors pass that reads them.
11
12
  *
12
13
  * @param {object} [config] - The resolved build configuration. Defaults to this
13
14
  * repository's.
@@ -16,6 +17,53 @@
16
17
  * caller that needs one, refuses that itself.
17
18
  */
18
19
  export function itemPackJsonDirs(config?: object): string[];
20
+ /**
21
+ * The passes to run, ordered so that each one follows the output it reads.
22
+ *
23
+ * **Declaration order is presentation, not compile order (#73).** The same
24
+ * `packs:` list is the manifest's `packs` array, which a consumer orders for a
25
+ * reader browsing compendiums; the actors pass, meanwhile, resolves each
26
+ * being's embedded items against the item passes' *output*. Making one list
27
+ * satisfy both meant an Actor pack declared first compiled only where a
28
+ * previous run had already left `build/packs-json` populated — green on a warm
29
+ * tree, exit 1 on every fresh checkout and every CI runner, and `build/` is
30
+ * gitignored so that is the state CI always starts from.
31
+ *
32
+ * **The reordering is the smallest one that works.** Each step takes the
33
+ * *earliest declared* pass whose dependencies are all already emitted, so a
34
+ * list that was already in a workable order comes back untouched, and one that
35
+ * was not moves exactly the passes that had to move. A dependency is satisfied
36
+ * only when **every** pack of that type has run: a being addresses an item by
37
+ * `(type, shortcode)` without knowing which Item pack ships it, so waiting for
38
+ * one of several would resolve some beings and silently fail others.
39
+ *
40
+ * A dependency on a type this configuration declares no pack of is not waited
41
+ * for. A package may ship an Actor pack and no Item pack; the pass that needs
42
+ * one refuses on its own, with a message about items rather than about order.
43
+ *
44
+ * @param {readonly object[]} packs - The passes to be run, as declared.
45
+ * @returns {object[]} A new list, in compile order. The input is untouched.
46
+ * @throws {Error} If the passes read each other's output in a cycle, which no
47
+ * order can satisfy. Only reachable from a mis-declared compiler, so it names
48
+ * the passes rather than blaming the pack list.
49
+ */
50
+ export function orderPassesByDependency(packs: readonly object[]): object[];
51
+ /**
52
+ * The dependencies this run cannot satisfy by ordering, because the pass that
53
+ * would produce them is not in it.
54
+ *
55
+ * Ordering answers the whole-package build; a run restricted to one pack
56
+ * (`content-build package compile <name>`) cannot conjure the passes it left
57
+ * out. Where their output is already on disk from an earlier run that is fine
58
+ * — it is how compiling one pack at a time is meant to work — so this reports
59
+ * only what is genuinely absent, and names the pack that would write it rather
60
+ * than the directory that is missing.
61
+ *
62
+ * @param {readonly object[]} running - The passes this run will execute.
63
+ * @param {object} config - The resolved build configuration.
64
+ * @returns {string[]} One message per unsatisfiable dependency.
65
+ */
66
+ export function unsatisfiedPassDependencies(running: readonly object[], config: object): string[];
19
67
  /**
20
68
  * The passes that compiled nothing when they were expected to compile
21
69
  * something — a build failure, not a quiet no-op.
@@ -0,0 +1,125 @@
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
+ * Every address a homepage carries, wherever it is written.
50
+ *
51
+ * **Both halves of the page are in scope, and that is the finding rather than
52
+ * the assumption.** Of the six homepages authored today, four carry every link
53
+ * in the body as ordinary markdown and two carry them in `landing:` — and the
54
+ * one whose dead links prompted the check has an *empty body*, so a body-only
55
+ * reading would have found nothing at all on it. A dead link in a card is
56
+ * exactly as broken as one in a paragraph.
57
+ *
58
+ * Three shapes are gathered, and the caller needs to tell them apart because
59
+ * the rules differ:
60
+ *
61
+ * - **`url`** — package-relative, resolved against the site by the theme.
62
+ * - **`href`** — already resolved, used verbatim.
63
+ * - **prose and body markdown links** — emitted as written and resolved by the
64
+ * browser against the landing's own address, which *is* the package root, so
65
+ * a relative one means the same thing a `url` does.
66
+ *
67
+ * `banner:` is not an address: it is an image path resolved through the CDN
68
+ * base, and `banner: none` is a sentinel rather than a target. Top-level
69
+ * `title` and `description` are not walked either — they are set as text, never
70
+ * rendered as markdown.
71
+ *
72
+ * @param {object|null|undefined} fm - The note's frontmatter.
73
+ * @param {string} [body] - The note's markdown body.
74
+ * @returns {Array<{field: string, url: string, kind: string}>} Every address,
75
+ * frontmatter first and then the body, each with the dotted path it was
76
+ * written at.
77
+ */
78
+ export function homepageAddresses(fm: object | null | undefined, body?: string): Array<{
79
+ field: string;
80
+ url: string;
81
+ kind: string;
82
+ }>;
83
+ /**
84
+ * The note type that compiles to the package homepage.
85
+ *
86
+ * @type {string}
87
+ */
88
+ export const HOMEPAGE_TYPE: string;
89
+ /**
90
+ * What a homepage note may write under `sohl:` — nothing.
91
+ *
92
+ * Empty on purpose, and declared rather than omitted: a type with no vocabulary
93
+ * and a type that is unknown are different findings, and only the second is an
94
+ * authoring error. The whole envelope is the two top-level keys `type` and an
95
+ * optional `title`; there is no game-system data on a page that compiles to no
96
+ * document.
97
+ *
98
+ * @type {readonly import("./field-spec.mjs").FieldSpec[]}
99
+ */
100
+ export const HOMEPAGE_FIELDS: readonly import("./field-spec.mjs").FieldSpec[];
101
+ /**
102
+ * Where a homepage is written, relative to the package's site root.
103
+ *
104
+ * Hugo's section landing, because the page *is* the package's landing: the
105
+ * package root is a section and this is its index.
106
+ *
107
+ * @type {string}
108
+ */
109
+ export const HOMEPAGE_DESTINATION: string;
110
+ /**
111
+ * The two frontmatter keys that hold an address, and what each one means.
112
+ *
113
+ * They are **not** interchangeable, and a check that treated them as one would
114
+ * be wrong about both. The theme resolves a `url` against the site with
115
+ * `relURL`, so a package writes `kb/rules/` and is served `/sohl/kb/rules/`
116
+ * without ever naming its own prefix. An `href` is an address that is *already*
117
+ * resolved and is used verbatim — which is what `cards.source: sections` fills
118
+ * in, since a section's permalink already carries the prefix.
119
+ *
120
+ * So a leading `/` is a defect in a `url` (it is prefixed a second time) and
121
+ * correct in an `href`.
122
+ *
123
+ * @type {ReadonlySet<string>}
124
+ */
125
+ export const HOMEPAGE_ADDRESS_KEYS: ReadonlySet<string>;
@@ -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[]>>;
@@ -56,6 +56,19 @@ export function configFromData(data: unknown, configPath: string): import("../co
56
56
  * compilers know neither what to compile nor where to put it.
57
57
  */
58
58
  export function loadPackConfig(): import("../config.mjs").ContentBuildConfig;
59
+ /**
60
+ * The file {@link loadPackConfig} resolved the configuration from.
61
+ *
62
+ * A diagnostic about a *configured* value has to name the file it was declared
63
+ * in, and re-deriving that path at the point of the finding would be a second
64
+ * resolution free to disagree with the first — the `PACKAGE_BUILD_CONFIG`
65
+ * override, the upward walk and the one-file-per-directory rule all have to
66
+ * come out the same way. This reports the path actually read.
67
+ *
68
+ * @returns {string} Its absolute path.
69
+ * @throws {Error} As {@link loadPackConfig}, when there is no configuration.
70
+ */
71
+ export function packConfigPath(): string;
59
72
  /** The stem every consuming repository declares its build under. */
60
73
  export const CONFIG_BASENAME: "package-build.config";
61
74
  /**
@@ -41,6 +41,49 @@ 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 no link resolution — so
69
+ * routing it through {@link renderPages} would buy it a pipeline it has no input
70
+ * for, and would make homepage-only mode depend on the index, the foreign
71
+ * manifests and the table universe that mode exists to not build.
72
+ *
73
+ * **Verbatim is the answer to #54, not a gap left by it.** A landing's links
74
+ * could not be *resolved* here without giving `homepage` mode the index its
75
+ * licensing fence exists to not build, so they are **checked** instead:
76
+ * {@link auditHomepageLinks} reads the `landing:` addresses and the body's
77
+ * markdown links, and reports a wikilink on the page rather than resolving one.
78
+ *
79
+ * @param {string} outRoot - The package's site root — the configured `site.out`,
80
+ * one level above the content mount.
81
+ * @param {readonly object[]} pages - From {@link collectHomepages}.
82
+ * @param {object} config - The resolved configuration, for the package name and
83
+ * the default title.
84
+ * @returns {number} How many pages were written.
85
+ */
86
+ export function writeHomepages(outRoot: string, pages: readonly object[], config: object): number;
44
87
  /**
45
88
  * The integrity gates a site build runs before it writes anything.
46
89
  *
@@ -63,6 +106,17 @@ export function collectTreePages(tree: object, ctx: object): {
63
106
  * @returns {object} The gate results and, when they pass, the built index.
64
107
  */
65
108
  export function siteGates(pages: object[], findings: object, { manifestDir }: object): object;
109
+ /**
110
+ * The gate result of a build that ran none of them.
111
+ *
112
+ * Homepage-only publishes one authored page and resolves nothing, so every gate
113
+ * here is about a surface that mode does not have. The shape is returned all the
114
+ * same, because a caller reads the same fields whichever mode ran and a `null`
115
+ * would make each of them a special case.
116
+ *
117
+ * @returns {object} An all-clear gate result.
118
+ */
119
+ export function emptyGates(): object;
66
120
  /** Whether any gate produced a finding. */
67
121
  export function gatesFailed(gates: any): boolean;
68
122
  /**
@@ -59,6 +59,60 @@ export function releaseUrls({ repoUrl, version, artifact }: {
59
59
  * @returns {object[]} The manifest's `packs` array.
60
60
  */
61
61
  export function manifestPacks(config: object): object[];
62
+ /**
63
+ * What `packFolders` and the derived `packs[]` disagree about.
64
+ *
65
+ * `packFolders` is the one **declared** manifest key that names something the
66
+ * build **derives**: every other declared key states a fact about the package
67
+ * (`title`, `socket`, `grid`) or addresses a staged file (`esmodules`,
68
+ * `styles`, `languages`), and a staged file is a different relation, checked
69
+ * against the stage rather than against configuration. So this is the one place
70
+ * a declaration can go stale against a value the build already computed — and
71
+ * until now nothing compared them (#81).
72
+ *
73
+ * `HarnMaster-3-FoundryVTT` shipped the consequence: its folder named four
74
+ * packs, three of which had not existed since the compendium was consolidated,
75
+ * and omitted `items` — 1,577 of 1,597 documents, loose in Foundry's compendium
76
+ * browser, with the build reporting nothing (HM3#420).
77
+ *
78
+ * **The two findings are not the same finding**, and giving them one severity
79
+ * gets one of them wrong:
80
+ *
81
+ * - _A folder names a pack that does not exist_ is an **error**. Foundry
82
+ * resolves the name against the package's own packs and silently skips what
83
+ * it cannot find, so the declaration does nothing at all; there is no
84
+ * arrangement in which it is intended, and the fix is unambiguous.
85
+ * - _A pack no folder names_ is a **warning**. It is legal and can be
86
+ * deliberate — a package may want one pack at the root — so failing on it
87
+ * would break working packages for a matter of taste. But a package that
88
+ * bothered to declare a folder rarely meant to leave one out, which is
89
+ * exactly how HM3's `items` went unnoticed.
90
+ * - _A package declaring no folders_ says **nothing**. Everything at the root
91
+ * is the majority arrangement, not an omission.
92
+ *
93
+ * Errors come first, in declaration order, then warnings in pack order: the
94
+ * unresolvable names are what a reader fixes, and a folder gaining a name often
95
+ * settles a warning too.
96
+ *
97
+ * @param {object} options
98
+ * @param {unknown} [options.packFolders] - The declared `packFolders`.
99
+ * @param {ReadonlyArray<{name: string}>} [options.packs] - The derived packs,
100
+ * as {@link manifestPacks} returns them.
101
+ * @returns {Array<{severity: "error"|"warning", message: string, pack: string,
102
+ * folder?: string, keyPath: Array<string|number>}>} The findings, ordered.
103
+ */
104
+ export function packFolderFindings({ packFolders, packs }: {
105
+ packFolders?: unknown;
106
+ packs?: readonly {
107
+ name: string;
108
+ }[] | undefined;
109
+ }): Array<{
110
+ severity: "error" | "warning";
111
+ message: string;
112
+ pack: string;
113
+ folder?: string;
114
+ keyPath: Array<string | number>;
115
+ }>;
62
116
  /**
63
117
  * The `relationships` block as published — every declared dependency, with the
64
118
  * build's own keys dropped.
@@ -109,20 +163,32 @@ export function buildManifest({ config, packageJson, artifact, flags }: {
109
163
  /**
110
164
  * Write the generated manifest into the staged package.
111
165
  *
166
+ * The declared `packFolders` is checked against the derived `packs[]` first,
167
+ * and an unresolvable name **stops the write**: a manifest already known to
168
+ * describe packs the package does not ship should not reach the stage, where
169
+ * the next command would deploy it (#81). See {@link packFolderFindings} for
170
+ * the rule and why its two findings carry different severities.
171
+ *
112
172
  * @param {object} options - As {@link buildManifest}, plus where to write.
113
173
  * @param {object} options.config - The resolved content configuration.
114
174
  * @param {object} options.packageJson - The repository's `package.json`.
115
175
  * @param {string} options.artifact - `system` or `module`.
116
176
  * @param {string} options.outDir - Directory to write into.
117
177
  * @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
178
+ * @param {string} [options.configFile] - Absolute path of the configuration
179
+ * file the manifest was resolved from, so a finding about it can be located.
180
+ * Omitting it costs the position, not the finding.
118
181
  * @returns {Promise<{path: string, manifest: object}>} Where it went, and what.
182
+ * @throws {Error} When a `packFolders` entry names a pack the package does not
183
+ * ship. Nothing is written in that case.
119
184
  */
120
- export function writeManifest({ config, packageJson, artifact, outDir, flags, }: {
185
+ export function writeManifest({ config, packageJson, artifact, outDir, flags, configFile, }: {
121
186
  config: object;
122
187
  packageJson: object;
123
188
  artifact: string;
124
189
  outDir: string;
125
190
  flags?: Record<string, object> | undefined;
191
+ configFile?: string | undefined;
126
192
  }): Promise<{
127
193
  path: string;
128
194
  manifest: object;