@heroiclands/package-build 21.2.0 → 22.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 CHANGED
@@ -1,5 +1,46 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - a278438: **A manifest's `url` is the package's homepage.** It is the Project Homepage link
8
+ a reader follows from the Foundry package listing _before_ installing anything,
9
+ so it answers "what is this?" — which the authored page at
10
+ `https://www.heroiclands.org/<contentPackage>/` does and a source tree does not.
11
+ `bugs`, `manifest` and `download` address release artefacts and stay on the
12
+ repository holding them. The address is derived from the content package rather
13
+ than declared, because the shared site-deploy workflow already publishes there; a
14
+ declared copy would be a second spelling of a settled fact, and the one free to
15
+ drift, since nothing fetches `url` the way Foundry fetches `manifest`.
16
+
17
+ **Breaking — `releaseUrls` takes `homeUrl`.** A call without it produced a
18
+ manifest advertising `undefined`, so the parameter is required rather than
19
+ defaulted. `packageHomepage` and `HOMEPAGE_ORIGIN` are exported beside it, and
20
+ `packageHomepage` refuses an empty name for the same reason.
21
+
22
+ **A configuration always names a content package**, so the manifest no longer
23
+ carries the branch that handled one that did not: the content index is advertised
24
+ unconditionally, which is what the comment above it already claimed.
25
+
26
+ ### Patch Changes
27
+
28
+ - dec6ef4: **A full-page picture prints where it was written** — In the book, an image
29
+ marked as full width now opens the page that follows the prose introducing it,
30
+ with the rest of that prose running below it. It can no longer appear above the
31
+ sentence that points at it, or a page later than the text that follows it.
32
+
33
+ - _A picture that asks for a float still floats_, which is what a float is for.
34
+ - _A picture taller than the page takes a page of its own._
35
+ - 32322bd: **Dependency bumps** — `package-build bump` is described by what it is for: npm
36
+ performs the resolution, so a version whose dependency set differs from the one
37
+ it replaces is taken as correctly as one that moves three lines, while patching
38
+ the lockfile by hand is right only while the two dependency sets match. npm
39
+ writes `package-lock.json` with the indentation `package.json` uses, so a
40
+ formatted manifest yields a formatted lockfile; the bump holds each file to the
41
+ indent it already carries, which is what covers a lockfile indented unlike its
42
+ manifest.
43
+
3
44
  ## 21.2.0
4
45
 
5
46
  ### Minor Changes
@@ -1137,10 +1137,11 @@ function e2eCommand() {
1137
1137
  * `bump [packages..]` — take a newer version of a dependency.
1138
1138
  *
1139
1139
  * npm does the resolving, so a bump whose dependency set changed is as correct
1140
- * as one that moves three lines. What this adds is the indentation: every
1141
- * repository here writes `package-lock.json` with four spaces and
1142
- * prettier-ignores it, and npm rewrites it with two, so the version change
1143
- * arrives buried in a whole-file reformat.
1140
+ * as one that moves three lines; patching the lockfile by hand is right only
1141
+ * while the two dependency sets match, and nothing says when they stop
1142
+ * matching. npm writes the lockfile with `package.json`'s indentation, and
1143
+ * this holds each file to the indent it already carried, so a lockfile indented
1144
+ * unlike its manifest keeps its own.
1144
1145
  *
1145
1146
  * Named nothing, it takes the first-party packages — the ones a person bumps
1146
1147
  * by hand, the moment a release publishes, usually to unblock the change that
@@ -1151,7 +1152,7 @@ function e2eCommand() {
1151
1152
  function bumpCommand() {
1152
1153
  return {
1153
1154
  command: "bump [packages..]",
1154
- describe: "Take a newer version of a dependency, keeping the lockfile's formatting",
1155
+ describe: "Take a newer published version of a declared dependency",
1155
1156
  builder: (y) =>
1156
1157
  y
1157
1158
  .positional("packages", {
package/docs/api.md CHANGED
@@ -1487,7 +1487,9 @@ console.log(normalizeRepoUrl("git@github.com:HeroicLands/sohl.git"));
1487
1487
  | ------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1488
1488
  | `ARTIFACTS` | `const ARTIFACTS` | — | reading the two Foundry package kinds (`"system"`, `"module"`) as the artifact name their manifest and release archive are called |
1489
1489
  | `normalizeRepoUrl` | `normalizeRepoUrl(repository)` | `string` — normalised `https://` URL, no trailing slash | normalising `package.json`'s `repository` field (object or shorthand string, `git+…git` or plain) to the exact URL Foundry fetches release assets from |
1490
- | `releaseUrls` | `releaseUrls({ repoUrl, version, artifact })` | `{url: string, bugs: string, manifest: string, download: string}` | building the manifest's release addresses `manifest` points at `releases/latest` so an installed package can discover updates, `download` is pinned to this version |
1490
+ | `HOMEPAGE_ORIGIN` | `HOMEPAGE_ORIGIN` | `string` the origin every package's homepage is served from | stating the site origin once, so the address a manifest advertises and the address the configuration documents cannot disagree |
1491
+ | `packageHomepage` | `packageHomepage(contentPackage)` | `string` — `<origin>/<contentPackage>/` | deriving the homepage a manifest's `url` points at; throws when given no name rather than advertising `<origin>/undefined/` |
1492
+ | `releaseUrls` | `releaseUrls({ repoUrl, homeUrl, version, artifact })` | `{url: string, bugs: string, manifest: string, download: string}` | building the manifest's addresses — `url` is the homepage a reader follows before installing, while `bugs`, `manifest` and `download` stay on the repository holding the artefacts |
1491
1493
  | `metadataUrl` | `metadataUrl({ repoUrl, version, contentPackage })` | `string` — the version-pinned asset URL | computing where this release publishes its content index, pinned to this version like `download` so a dependency's manifest and its fetched index always describe the same release |
1492
1494
  | `manifestPacks` | `manifestPacks(config)` | `object[]` | deriving the manifest's `packs` array from the one pack list the build already has, in build order, companions flattened in |
1493
1495
  | `packFolderFindings` | `packFolderFindings({ packFolders, packs = [] })` | `Array<{severity: "error"\|"warning", message: string, pack: string, folder?: string, keyPath: Array<string\|number>}>` | checking a declared `packFolders` against the derived pack list — a folder naming a pack that does not exist is an error, a pack no folder names is a warning |
package/docs/commands.md CHANGED
@@ -441,7 +441,7 @@ None.
441
441
 
442
442
  **NAME**
443
443
 
444
- Take a newer version of a dependency, keeping the lockfile's formatting.
444
+ Take a newer published version of a declared dependency.
445
445
 
446
446
  **SYNOPSIS**
447
447
 
@@ -453,17 +453,20 @@ package-build bump [packages..] [--tag <tag>] [--check]
453
453
 
454
454
  Takes the newest published version of one or more declared dependencies,
455
455
  updating `package-lock.json` — and `package.json` when the declared range has to
456
- move — then restoring both files to the indentation they already used.
456
+ move — and holding both files to the indentation they already carry.
457
457
 
458
- npm performs the resolution, so a version whose dependency set differs from the
459
- one it replaces is handled as correctly as one that moves three lines. Editing
460
- those three lines by hand is only right while the two dependency sets are
461
- identical, and nothing tells the author when they are not.
458
+ npm performs the resolution, which is what this exists for. A version whose
459
+ dependency set differs from the one it replaces is handled as correctly as one
460
+ that moves three lines, while editing the `version` / `resolved` / `integrity`
461
+ lines by hand is only right while the two dependency sets are identical — and
462
+ nothing tells the author when they are not.
462
463
 
463
- The indentation is the reason this exists rather than `npm install` being run
464
- directly. Every repository consuming this toolchain writes `package-lock.json`
465
- with four spaces and prettier-ignores it; npm rewrites it with two, turning a
466
- three-line version change into a whole-file reformat no reviewer can read past.
464
+ npm writes `package-lock.json` with the indentation `package.json` uses, so a
465
+ consumer whose manifest is formatted gets a formatted lockfile back and the
466
+ diff is the version change alone. The indentation step covers the case where
467
+ the two files disagree: each is written back with the indent it already
468
+ carried, rather than the lockfile taking the manifest's. A run that had to put
469
+ an indent back names the file it rewrote.
467
470
 
468
471
  Named no packages, it takes every `@heroiclands/*` dependency the manifest
469
472
  declares. That scope is the one a person bumps by hand — a first-party release
@@ -496,7 +499,6 @@ every package is already current.
496
499
  ```
497
500
  $ package-build bump
498
501
  @heroiclands/package-build 20.6.0 → 20.7.0
499
- kept the existing indentation of package-lock.json
500
502
 
501
503
  Install it with `npm ci`, which resolves from the lockfile this just moved.
502
504
  ```
@@ -1754,8 +1754,17 @@ not. The website and a Foundry journal get true CSS wrap from the same
1754
1754
  directive. Expect the same statement, not the same page.
1755
1755
 
1756
1756
  An image with no `float:` is an ordinary block in the flow, where it was
1757
- written. A `.full-width` image is placed at the top of the page it falls on,
1758
- since a block cannot leave the column it is set in.
1757
+ written. A `.full-width` image is set at the **top of a new page**: the book is
1758
+ set in two columns, only a float spans them both, and a float is placed where
1759
+ the page has room rather than where it was written — so the page breaks before
1760
+ the picture, and the prose that follows it runs below it on the same page. The
1761
+ picture can therefore never print above the prose that introduces it or after
1762
+ the prose that follows it, at the cost of the page before it ending short. A
1763
+ picture taller than the page takes a page of its own.
1764
+
1765
+ A `.full-width` image that **also states a `float:`** is asking for a float and
1766
+ gets one, with no break before it. A float is placed where it fits, which may be
1767
+ the next page.
1759
1768
 
1760
1769
  ##### Both vocabularies are closed
1761
1770
 
@@ -12,19 +12,17 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Taking a newer version of a dependency, without reformatting the lockfile.
15
+ * Taking a newer version of a dependency.
16
16
  *
17
- * npm resolves the bump; this module's whole job is what npm does badly here.
18
- * Every repository in the constellation indents `package-lock.json` with four
19
- * spaces and prettier-ignores it, and npm rewrites it with two so a
20
- * three-line version change arrives as a seventeen-thousand-line diff that no
21
- * reviewer can read past.
17
+ * npm does the resolution, and that is the point of the command: editing the
18
+ * three `version` / `resolved` / `integrity` lines by hand is correct only
19
+ * while the new version's dependency set is identical to the old one's, and
20
+ * nothing tells the author when it is not. npm is what knows.
22
21
  *
23
- * The alternative editing the three `version` / `resolved` / `integrity`
24
- * lines by hand is only correct while the new version's dependency set is
25
- * identical to the old one's, and nothing tells the author when it is not. npm
26
- * is what knows; so npm does the resolution and the indentation is restored
27
- * afterwards.
22
+ * npm writes `package-lock.json` with the indentation `package.json` uses, so
23
+ * a consumer whose two files agree sees only the lines that moved. Each file
24
+ * is written back with the indent it already carried, so a lockfile indented
25
+ * unlike its manifest keeps its own.
28
26
  */
29
27
 
30
28
  import fs from "node:fs";
@@ -121,8 +119,8 @@ export function lockedVersion(lock, name) {
121
119
  * Take the newest published version of one or more dependencies.
122
120
  *
123
121
  * npm performs the resolution — so a bump that changes the dependency set is
124
- * as correct as one that moves three lines — and both JSON files are restored
125
- * to the indentation they already used.
122
+ * as correct as one that moves three lines — and both JSON files are written
123
+ * back with the indentation they already used.
126
124
  *
127
125
  * @param {object} options - Options.
128
126
  * @param {string} options.rootDir - The repository root holding `package.json`.
@@ -196,9 +194,9 @@ export function bumpDependencies({ rootDir, packages, tag = "latest", check = fa
196
194
  rootDir,
197
195
  );
198
196
 
199
- // Restore what npm reformatted. Both files, because npm rewrites the
200
- // manifest too when a range has to move — which below 1.0 it always does,
201
- // a caret there being locked to the minor.
197
+ // Hold each file to the indent it already carried. Both, because npm
198
+ // rewrites the manifest too when a range has to move — which below 1.0 it
199
+ // always does, a caret there being locked to the minor.
202
200
  const reindented = [];
203
201
  if (reindentJsonFile(lockPath, detectJsonIndent(lockText))) reindented.push(lockPath);
204
202
  if (reindentJsonFile(manifestPath, detectJsonIndent(manifestText))) {
@@ -667,14 +667,22 @@ function renderLink(href, inner, ctx) {
667
667
  * ## The width class is the measure
668
668
  *
669
669
  * An image with no class is one column wide. That is `width: 100%` of whatever
670
- * container it is set in — the page today, and a column once the book is set in
671
- * two — so the ordinary case needs nothing but an ordinary block and stays
672
- * correct through the change.
673
- *
674
- * `.full-width` has to leave its column, and a block cannot: only a float
675
- * placed with `scope: "parent"` spans every column of the page. So a
676
- * full-width image is always placed, whether or not it states a `float:`, and
677
- * an image that states neither is left in the flow where it was written.
670
+ * container it is set in — a column of the two the body is set in — so the
671
+ * ordinary case needs nothing but an ordinary block, and lands exactly where it
672
+ * was written.
673
+ *
674
+ * `.full-width` has to leave its column, and only a float placed with
675
+ * `scope: "parent"` spans every column of a page. A float, though, is placed
676
+ * where the page has room rather than where it was written: it is set at the
677
+ * top of the page, above the prose that introduces it, and where the page is
678
+ * too far along to take it, on the next page — after prose that follows it in
679
+ * the note. Document order governs what follows an image, so
680
+ * {@link bookTypstPreamble}'s `book-figure` breaks the page first and places
681
+ * the figure at the top of the fresh one, where nothing is above it to displace
682
+ * it and nothing that follows it can print first.
683
+ *
684
+ * A `.full-width` image that **also states a `float:`** is asking for a float,
685
+ * and keeps one — deferral is the honest consequence of the request.
678
686
  *
679
687
  * ## A float occupies the measure
680
688
  *
@@ -712,8 +720,10 @@ function renderImage(token, ctx) {
712
720
  // In the flow where it was written: no class asking for the page, and no
713
721
  // position asking for the top or the bottom of the column.
714
722
  if (!float && scope === "column") return `\n${figure}\n\n`;
715
- const align = float?.align ?? "top";
716
- return `\n#place(${align}, float: true, scope: "${scope}", clearance: 0.7em)[\n${figure}\n]\n\n`;
723
+ // The page, in document order: a width class says how wide the picture is
724
+ // and not when it appears.
725
+ if (!float) return `\n#book-figure[\n${figure}\n]\n\n`;
726
+ return `\n#place(${float.align}, float: true, scope: "${scope}", clearance: 0.7em)[\n${figure}\n]\n\n`;
717
727
  }
718
728
 
719
729
  /**
@@ -904,6 +914,26 @@ export function bookTypstPreamble() {
904
914
  " place(top + left)[#book-plate(kicker, title, banner, 9cm, body)]\n" +
905
915
  " pagebreak()\n" +
906
916
  "}",
917
+ // A full-width figure spans the page in document order. Only a float
918
+ // spans every column, and a float is placed where the page has room
919
+ // rather than where it was written — at the top, above the prose that
920
+ // introduces it, or on the next page when this one is too far along.
921
+ // Breaking first puts it at the top of a page whose float region is
922
+ // empty, which is the one place it cannot be displaced. A picture
923
+ // taller than the page takes a page of its own, for the reason
924
+ // `book-wide` states.
925
+ //
926
+ // `measure` alone is stable here. A rule reading `here().position()`
927
+ // to keep the break for the cases that need it does not converge: the
928
+ // position decides the layout and the layout decides the position.
929
+ "#let book-figure(body) = context {\n" +
930
+ " if measure(block(width: book-text-width)[#body]).height >= book-text-height * 0.88 {\n" +
931
+ " page(columns: 1)[#body]\n" +
932
+ " } else {\n" +
933
+ " pagebreak(weak: true)\n" +
934
+ ' place(top, float: true, scope: "parent", clearance: 0.7em)[#body]\n' +
935
+ " }\n" +
936
+ "}",
907
937
  // Wide content spans the page, and how it spans depends on how tall it
908
938
  // is: a float is unbreakable and silently overflows, so anything taller
909
939
  // than a page takes pages of its own instead.
package/manifest.mjs CHANGED
@@ -94,6 +94,49 @@ export function normalizeRepoUrl(repository) {
94
94
  return url;
95
95
  }
96
96
 
97
+ /**
98
+ * Where every HeroicLands package's homepage is served from.
99
+ *
100
+ * Stated once here and read by {@link packageHomepage}, so the origin a
101
+ * manifest advertises and the origin the configuration documents cannot come
102
+ * to disagree.
103
+ *
104
+ * @type {string}
105
+ */
106
+ export const HOMEPAGE_ORIGIN = "https://www.heroiclands.org";
107
+
108
+ /**
109
+ * A package's homepage — the page `url` sends a reader to.
110
+ *
111
+ * Derived from the content package rather than declared, because the address is
112
+ * already fixed: every package publishes an authored homepage at
113
+ * `<origin>/<contentPackage>/`, and the shared site-deploy workflow publishes
114
+ * it there. A declared copy would be a second spelling of a settled fact, free
115
+ * to drift — and the one that drifts is this one, because nothing fetches `url`
116
+ * the way Foundry fetches `manifest`, so a wrong value is never reported.
117
+ *
118
+ * The site's own `baseURL` says the same thing, and is deliberately not the
119
+ * source: it sits at `site/hugo.toml` in most repositories and `kb/hugo.toml`
120
+ * in the system, so reading it would mean the packaging half knowing where each
121
+ * repository keeps its site configuration.
122
+ *
123
+ * @param {string} contentPackage - The package's content-package name.
124
+ * @returns {string} The homepage URL, with its trailing slash.
125
+ * @throws {TypeError} When no name is given. Interpolating a missing one yields
126
+ * `<origin>/undefined/`, a URL that resolves and is wrong — and nothing
127
+ * fetches `url`, so it would be advertised for as long as nobody clicked it.
128
+ */
129
+ export function packageHomepage(contentPackage) {
130
+ const name = String(contentPackage ?? "").trim();
131
+ if (!name) {
132
+ throw new TypeError(
133
+ "the configuration declares no `contentPackage`, so the manifest has " +
134
+ "no homepage to advertise.",
135
+ );
136
+ }
137
+ return `${HOMEPAGE_ORIGIN}/${name}/`;
138
+ }
139
+
97
140
  /**
98
141
  * The addresses a Foundry manifest advertises.
99
142
  *
@@ -103,15 +146,23 @@ export function normalizeRepoUrl(repository) {
103
146
  * install at that release forever. `download` points at this exact version,
104
147
  * because that is the archive this manifest describes.
105
148
  *
149
+ * **`url` is the homepage, not the repository.** It is the Project Homepage
150
+ * link a reader follows from the package listing *before* installing anything,
151
+ * so it answers "what is this?" — which a page written for that question does
152
+ * and a source tree does not. `bugs`, `manifest` and `download` address release
153
+ * artefacts and stay on the repository that holds them.
154
+ *
106
155
  * @param {object} opts
107
156
  * @param {string} opts.repoUrl - Normalised repository URL.
157
+ * @param {string} opts.homeUrl - The package's homepage, from
158
+ * {@link packageHomepage}.
108
159
  * @param {string} opts.version - The version being built.
109
160
  * @param {"system"|"module"} opts.artifact - Which artifact is shipped.
110
161
  * @returns {{url: string, bugs: string, manifest: string, download: string}}
111
162
  */
112
- export function releaseUrls({ repoUrl, version, artifact }) {
163
+ export function releaseUrls({ repoUrl, homeUrl, version, artifact }) {
113
164
  return {
114
- url: repoUrl,
165
+ url: homeUrl,
115
166
  bugs: `${repoUrl}/issues`,
116
167
  manifest: `${repoUrl}/releases/latest/download/${artifact}.json`,
117
168
  download: `${repoUrl}/releases/download/v${version}/${artifact}.zip`,
@@ -434,7 +485,12 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
434
485
  id: config.foundryPackage,
435
486
  version: packageJson.version,
436
487
  packs: manifestPacks(config),
437
- ...releaseUrls({ repoUrl, version: packageJson.version, artifact }),
488
+ ...releaseUrls({
489
+ repoUrl,
490
+ homeUrl: packageHomepage(config.contentPackage),
491
+ version: packageJson.version,
492
+ artifact,
493
+ }),
438
494
  };
439
495
  if (config.compatibility) derived.compatibility = config.compatibility;
440
496
 
@@ -482,16 +538,14 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
482
538
  // A flag rather than a top-level key because Foundry's manifest schema is
483
539
  // closed and `flags` is its declared extension point; an unknown key at the
484
540
  // top level is dropped by some readers and rejected by others.
485
- if (config.contentPackage) {
486
- merged.flags = {
487
- ...(declared.flags ?? {}),
488
- metadataUrl: metadataUrl({
489
- repoUrl,
490
- version: packageJson.version,
491
- contentPackage: config.contentPackage,
492
- }),
493
- };
494
- }
541
+ merged.flags = {
542
+ ...(declared.flags ?? {}),
543
+ metadataUrl: metadataUrl({
544
+ repoUrl,
545
+ version: packageJson.version,
546
+ contentPackage: config.contentPackage,
547
+ }),
548
+ };
495
549
 
496
550
  if (flags && Object.keys(flags).length) {
497
551
  merged.flags = { ...(merged.flags ?? declared.flags ?? {}) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "21.2.0",
3
+ "version": "22.0.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -57,8 +57,8 @@ export function lockedVersion(lock: object, name: string): string | undefined;
57
57
  * Take the newest published version of one or more dependencies.
58
58
  *
59
59
  * npm performs the resolution — so a bump that changes the dependency set is
60
- * as correct as one that moves three lines — and both JSON files are restored
61
- * to the indentation they already used.
60
+ * as correct as one that moves three lines — and both JSON files are written
61
+ * back with the indentation they already used.
62
62
  *
63
63
  * @param {object} options - Options.
64
64
  * @param {string} options.rootDir - The repository root holding `package.json`.
@@ -17,6 +17,28 @@
17
17
  export function normalizeRepoUrl(repository: string | {
18
18
  url?: string;
19
19
  }): string;
20
+ /**
21
+ * A package's homepage — the page `url` sends a reader to.
22
+ *
23
+ * Derived from the content package rather than declared, because the address is
24
+ * already fixed: every package publishes an authored homepage at
25
+ * `<origin>/<contentPackage>/`, and the shared site-deploy workflow publishes
26
+ * it there. A declared copy would be a second spelling of a settled fact, free
27
+ * to drift — and the one that drifts is this one, because nothing fetches `url`
28
+ * the way Foundry fetches `manifest`, so a wrong value is never reported.
29
+ *
30
+ * The site's own `baseURL` says the same thing, and is deliberately not the
31
+ * source: it sits at `site/hugo.toml` in most repositories and `kb/hugo.toml`
32
+ * in the system, so reading it would mean the packaging half knowing where each
33
+ * repository keeps its site configuration.
34
+ *
35
+ * @param {string} contentPackage - The package's content-package name.
36
+ * @returns {string} The homepage URL, with its trailing slash.
37
+ * @throws {TypeError} When no name is given. Interpolating a missing one yields
38
+ * `<origin>/undefined/`, a URL that resolves and is wrong — and nothing
39
+ * fetches `url`, so it would be advertised for as long as nobody clicked it.
40
+ */
41
+ export function packageHomepage(contentPackage: string): string;
20
42
  /**
21
43
  * The addresses a Foundry manifest advertises.
22
44
  *
@@ -26,14 +48,23 @@ export function normalizeRepoUrl(repository: string | {
26
48
  * install at that release forever. `download` points at this exact version,
27
49
  * because that is the archive this manifest describes.
28
50
  *
51
+ * **`url` is the homepage, not the repository.** It is the Project Homepage
52
+ * link a reader follows from the package listing *before* installing anything,
53
+ * so it answers "what is this?" — which a page written for that question does
54
+ * and a source tree does not. `bugs`, `manifest` and `download` address release
55
+ * artefacts and stay on the repository that holds them.
56
+ *
29
57
  * @param {object} opts
30
58
  * @param {string} opts.repoUrl - Normalised repository URL.
59
+ * @param {string} opts.homeUrl - The package's homepage, from
60
+ * {@link packageHomepage}.
31
61
  * @param {string} opts.version - The version being built.
32
62
  * @param {"system"|"module"} opts.artifact - Which artifact is shipped.
33
63
  * @returns {{url: string, bugs: string, manifest: string, download: string}}
34
64
  */
35
- export function releaseUrls({ repoUrl, version, artifact }: {
65
+ export function releaseUrls({ repoUrl, homeUrl, version, artifact }: {
36
66
  repoUrl: string;
67
+ homeUrl: string;
37
68
  version: string;
38
69
  artifact: "system" | "module";
39
70
  }): {
@@ -223,6 +254,16 @@ export function writeManifest({ config, packageJson, artifact, outDir, flags, co
223
254
  * convention this project is free to choose.
224
255
  */
225
256
  export const ARTIFACTS: readonly string[];
257
+ /**
258
+ * Where every HeroicLands package's homepage is served from.
259
+ *
260
+ * Stated once here and read by {@link packageHomepage}, so the origin a
261
+ * manifest advertises and the origin the configuration documents cannot come
262
+ * to disagree.
263
+ *
264
+ * @type {string}
265
+ */
266
+ export const HOMEPAGE_ORIGIN: string;
226
267
  /**
227
268
  * Relationship keys that direct the **build**, rather than describe the
228
269
  * package.