@heroiclands/package-build 6.0.0 → 7.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 +798 -0
- package/CONTENT.md +228 -4
- package/bin/content-build.mjs +196 -10
- package/bin/package-build.mjs +8 -1
- package/config.mjs +25 -3
- package/content-config.mjs +283 -29
- package/engine/address-diff.mjs +290 -0
- package/engine/base-compiler.mjs +25 -0
- package/engine/content-links.mjs +132 -27
- package/engine/content-lint.mjs +23 -2
- package/engine/diagnostics.mjs +61 -1
- package/engine/frontmatter-lint.mjs +22 -0
- package/engine/generate.mjs +10 -5
- package/engine/helpers.mjs +38 -0
- package/engine/homepage.mjs +206 -2
- package/engine/journals.mjs +8 -1
- package/engine/macros.mjs +2 -0
- package/engine/pack-config.mjs +143 -13
- package/engine/prose-lint.mjs +10 -2
- package/engine/scenes.mjs +2 -2
- package/engine/site-build.mjs +74 -19
- package/engine/web-wikilinks.mjs +13 -4
- package/engine/wikilink-syntax.mjs +25 -0
- package/engine/wikilinks.mjs +6 -3
- package/manifest.mjs +37 -2
- package/package.json +5 -3
- package/sohl/actors.mjs +23 -10
- package/sohl/items.mjs +1 -1
- package/types/content-config.d.mts +14 -0
- package/types/engine/address-diff.d.mts +108 -0
- package/types/engine/base-compiler.d.mts +18 -1
- package/types/engine/content-links.d.mts +11 -3
- package/types/engine/content-lint.d.mts +4 -1
- package/types/engine/diagnostics.d.mts +33 -1
- package/types/engine/generate.d.mts +3 -2
- package/types/engine/helpers.d.mts +29 -3
- package/types/engine/homepage.d.mts +117 -0
- package/types/engine/journals.d.mts +7 -1
- package/types/engine/pack-config.d.mts +22 -0
- package/types/engine/prose-lint.d.mts +10 -2
- package/types/engine/site-build.d.mts +28 -3
- package/types/engine/wikilink-syntax.d.mts +24 -0
- package/types/sohl/actors.d.mts +3 -3
package/CONTENT.md
CHANGED
|
@@ -155,7 +155,9 @@ the file sits in, fills the optional halves with their defaults
|
|
|
155
155
|
manifest switches off and `publish.site` at its `homepage` floor),
|
|
156
156
|
derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
|
|
157
157
|
freezes the result. A malformed configuration throws a `TypeError` naming the
|
|
158
|
-
offending field
|
|
158
|
+
offending field — and the line and column it was written on, in the
|
|
159
|
+
[located form](#diagnostics) — so it fails at load rather than as an empty pack
|
|
160
|
+
much later.
|
|
159
161
|
|
|
160
162
|
**Four values are derived rather than authored**, because each is something a
|
|
161
163
|
file can be asked for rather than told:
|
|
@@ -474,6 +476,7 @@ npx content-build markdown [paths..] [--fix]
|
|
|
474
476
|
npx content-build manifest [root] [--out <dir>]
|
|
475
477
|
npx content-build site [--out <dir>]
|
|
476
478
|
npx content-build reachability <dir> [file] [--index <shortcode>]
|
|
479
|
+
npx content-build addresses diff --from <zip|dir> [--strict]
|
|
477
480
|
```
|
|
478
481
|
|
|
479
482
|
| Command | What it does |
|
|
@@ -487,6 +490,7 @@ npx content-build reachability <dir> [file] [--index <shortcode>]
|
|
|
487
490
|
| `manifest` | Emit this package's cross-package link manifest. See [Publishing a link manifest](#publishing-a-link-manifest). |
|
|
488
491
|
| `site` | Publish the content tree as a website. See [Publishing a website](#publishing-a-website). |
|
|
489
492
|
| `reachability` | Walk outward from an index note and report what no path reaches, for a tree meant to be navigable from one entry point. |
|
|
493
|
+
| `addresses` | Report every published item address this build has stopped publishing. See [Diffing published addresses](#diffing-published-addresses). |
|
|
490
494
|
|
|
491
495
|
Every path, pack name and root it needs comes from the consuming repository's
|
|
492
496
|
`package-build.config.yaml`, so the usual invocation takes no arguments beyond
|
|
@@ -505,7 +509,7 @@ npx content-build lint # the configured `paths.content`
|
|
|
505
509
|
npx content-build lint some/tree # or a tree named outright
|
|
506
510
|
```
|
|
507
511
|
|
|
508
|
-
Checks the
|
|
512
|
+
Checks the three rules every note's **identity** is authored against, and reports
|
|
509
513
|
each finding in the located form below:
|
|
510
514
|
|
|
511
515
|
- **Shape** — a `shortcode` is strictly ASCII-alphanumeric. It is the identity
|
|
@@ -514,6 +518,8 @@ each finding in the located form below:
|
|
|
514
518
|
- **Uniqueness** — `(type, shortcode)` names one note. A document is addressed
|
|
515
519
|
across _every_ pack of its document type, so routing two same-address notes to
|
|
516
520
|
different packs with `pack:` does not separate them.
|
|
521
|
+
- **The package's own address** — exactly one note claims `/<package>/`. See
|
|
522
|
+
[Exactly one homepage](#exactly-one-homepage) below.
|
|
517
523
|
|
|
518
524
|
It compiles nothing, opens no LevelDB and needs no Foundry manifest, so it runs
|
|
519
525
|
in about a second and can gate a commit. An empty or untyped tree **fails**
|
|
@@ -527,6 +533,48 @@ addressed by the package rather than by a slug — so a package in
|
|
|
527
533
|
permanently unkeyed. That tree passes; a tree holding no notes at all still
|
|
528
534
|
fails.
|
|
529
535
|
|
|
536
|
+
### Exactly one homepage
|
|
537
|
+
|
|
538
|
+
A content tree declares **exactly one** `type: homepage` note (#52). Zero is an
|
|
539
|
+
error and two is an error, at the same severity, because they are one defect: a
|
|
540
|
+
package whose front page is not the page a person chose.
|
|
541
|
+
|
|
542
|
+
- _Zero_ and the package serves nothing at `/<package>/` — the failure the
|
|
543
|
+
authored homepage exists to prevent, and a silent one: the site build reports
|
|
544
|
+
`wrote 0 homepage(s)` and exits 0.
|
|
545
|
+
- _Two_ and it serves a page nobody chose. Every homepage is written to the same
|
|
546
|
+
`_index.md`, so the second overwrites the first and the front page is decided
|
|
547
|
+
by the order the walk reached the files in — by _filename_, on a type whose
|
|
548
|
+
whole point is that it is routed by frontmatter. There is no "first wins"
|
|
549
|
+
convention to fall back on, so nothing can pick the right one.
|
|
550
|
+
|
|
551
|
+
Neither has a safe default, so neither is a warning: a build that proceeded past
|
|
552
|
+
either would publish the wrong front page while reporting success, which is
|
|
553
|
+
exactly what a warning tolerates.
|
|
554
|
+
|
|
555
|
+
**Where it fires: `lint` _and_ `site`.** No single command reaches every
|
|
556
|
+
package — `HarnMaster-3-FoundryVTT` runs `content-build site` and no
|
|
557
|
+
`content-build lint`; `sohl-thalorna` runs `content-build lint` and its own site
|
|
558
|
+
builder — so a rule in one of them is a rule two of the six packages do not
|
|
559
|
+
have. Both call the same function, so there is one rule and two call sites
|
|
560
|
+
rather than two rules. In the site build it runs **before the output tree is
|
|
561
|
+
cleared**, so a failing gate cannot destroy a good site to report a bad tree.
|
|
562
|
+
|
|
563
|
+
**It does not vary by `publish.site`.** That setting chooses whether the
|
|
564
|
+
_content_ surfaces are published; the homepage is the floor beneath both modes.
|
|
565
|
+
The lint call site reads no `site:` block at all, and so could not vary by mode
|
|
566
|
+
even if the rule wanted to.
|
|
567
|
+
|
|
568
|
+
Zero has no file to name, so the locator is the **content root** — a real path,
|
|
569
|
+
and the directory the note has to be added to. No line or column is invented for
|
|
570
|
+
it. Two is reported once per offending note, located at its own `type:` value and
|
|
571
|
+
naming the other, because each note is a place an author has to open and edit:
|
|
572
|
+
|
|
573
|
+
```text
|
|
574
|
+
assets/content: error: holds no `type: homepage` note, so package "sohl" publishes nothing at its own address /sohl/ — a package's front page is one authored note in this tree, routed by `type:` rather than by filename
|
|
575
|
+
assets/content/homepage.md:3:7: error: duplicate `type: homepage` note, also declared by assets/content/Landing.md; a package has one front page, at /sohl/, and every homepage is written to the same `_index.md` — so the one the walk reaches last silently overwrites the rest
|
|
576
|
+
```
|
|
577
|
+
|
|
530
578
|
### Frontmatter, against the schema its type declares
|
|
531
579
|
|
|
532
580
|
The same command also checks that each note's `sohl:` block is what its **type**
|
|
@@ -570,6 +618,55 @@ does not exist. Removing it was verified output-neutral first: across 1,735
|
|
|
570
618
|
stripped notes, `package compile` produced byte-identical `build/packs-json` and
|
|
571
619
|
the site build byte-identical `site/content`.
|
|
572
620
|
|
|
621
|
+
### The homepage carries no address of its own
|
|
622
|
+
|
|
623
|
+
A note's URL derives from `name.full` and its identity from
|
|
624
|
+
`(type, shortcode)`. The homepage is the one page for which neither holds: it
|
|
625
|
+
publishes at `/<package>/`, fixed by the package id. So `content-build lint`
|
|
626
|
+
**refuses** `name`, `shortcode` and `id` on a `type: homepage` note (#53) rather
|
|
627
|
+
than ignoring them — an author fluent in the conventions writes them here
|
|
628
|
+
expecting exactly what they do everywhere else, and gets none of it.
|
|
629
|
+
|
|
630
|
+
They were never inert, which is why ignoring them was the wrong answer. A
|
|
631
|
+
`shortcode` puts the note in the address index and in the `dataview` link
|
|
632
|
+
universe, so `[[homepage-<shortcode>]]` resolves _green_ — to `homepage/<slug>/`,
|
|
633
|
+
an address derived from `name.full` that the site build never writes, because a
|
|
634
|
+
homepage goes to `_index.md` at the package root. A build reporting a live link
|
|
635
|
+
to a 404 is worse than one saying nothing. It also inflates this command's own
|
|
636
|
+
address tally, so the lint and the link manifest disagree about what the package
|
|
637
|
+
publishes: SoHL's tree reports `1607 address(es) across 1607 note(s)` with a
|
|
638
|
+
`shortcode` on its landing and `1606 address(es) across 1607 note(s)` without
|
|
639
|
+
one. That is one defect, not two — the tally is only ever printed on a clean run,
|
|
640
|
+
so refusing the field is what makes the count honest.
|
|
641
|
+
|
|
642
|
+
Each finding is located at the offending key and says what the field would have
|
|
643
|
+
decided, not merely that it does not belong:
|
|
644
|
+
|
|
645
|
+
```text
|
|
646
|
+
assets/content/homepage.md:26:1: error: `shortcode` decides nothing on a `type: homepage` note: this page's address is the package's own, `/<package>/`, fixed by the package id. It is not ignored either — it puts the note in the address index, so `[[homepage-<shortcode>]]` resolves to a page the site build never writes. Delete it
|
|
647
|
+
assets/content/homepage.md:28:1: error: `name` decides nothing on a `type: homepage` note: a page's slug derives from `name.full`, and a homepage's destination is fixed — it is written to `_index.md` at the package's own address, `/<package>/`. Write `title:` for what the page is called, and delete `name`
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**A named class, not an allow-list.** The documented envelope is `type` plus an
|
|
651
|
+
optional `title`, with `landing`, `description` and `banner` legitimate beside
|
|
652
|
+
them — but an unknown top-level key is **not** refused, and that boundary is the
|
|
653
|
+
decision rather than an omission. A homepage's frontmatter is emitted into the
|
|
654
|
+
published page, so an unrecognised key is a Hugo or theme parameter this build
|
|
655
|
+
has never heard of and has no standing to reject; a closed list would make every
|
|
656
|
+
new theme parameter wait on a package-build release. What is refused is the
|
|
657
|
+
specific class that makes a false claim about _where this page is_. `aliases` is
|
|
658
|
+
not in that class either: it is already dropped from every emitted page, so
|
|
659
|
+
authoring one here is the same no-op it is anywhere else.
|
|
660
|
+
|
|
661
|
+
**Where it fires: `content-build lint` only.** Unlike a rule about the shape of
|
|
662
|
+
the _tree_, which the site build has its own reason to gate on, this is a
|
|
663
|
+
_frontmatter-schema_ rule and `content-build site` runs none of them — wiring in one type's field
|
|
664
|
+
rule would have the site build refuse `shortcode` on a homepage while accepting
|
|
665
|
+
`weight: heavy` on a weapon. The gap that leaves is `HarnMaster-3-FoundryVTT`,
|
|
666
|
+
which runs no `content-build lint` at all and so receives no frontmatter finding
|
|
667
|
+
of any kind; that is a missing script in that repository, not a rule to duplicate
|
|
668
|
+
one at a time.
|
|
669
|
+
|
|
573
670
|
### The homepage's own links
|
|
574
671
|
|
|
575
672
|
The homepage is the page a reader arrives at, and until #54 it was the one page
|
|
@@ -789,6 +886,10 @@ title: HârnMaster Kethira Basic # optional; defaults to packageBuild.manifest.t
|
|
|
789
886
|
What the module is, which system it needs, how to install it.
|
|
790
887
|
```
|
|
791
888
|
|
|
889
|
+
A package declares **exactly one** of these, and both `content-build lint` and
|
|
890
|
+
`content-build site` require it — see
|
|
891
|
+
[Exactly one homepage](#exactly-one-homepage).
|
|
892
|
+
|
|
792
893
|
That is the whole envelope. A homepage **compiles into no compendium
|
|
793
894
|
document**, appears in no pack and in no link manifest, and is addressed by the
|
|
794
895
|
_package_ rather than by its own name — so `name.full`, `shortcode` and `id`
|
|
@@ -853,7 +954,10 @@ site:
|
|
|
853
954
|
trees:
|
|
854
955
|
- { from: kb/dev-docs, section: dev-docs }
|
|
855
956
|
sections:
|
|
856
|
-
being:
|
|
957
|
+
being:
|
|
958
|
+
title: Beings
|
|
959
|
+
banner: banners/creature.webp
|
|
960
|
+
description: Folk, animals and the things that walk the world.
|
|
857
961
|
readmeSections:
|
|
858
962
|
dev-docs: { title: Developer Documentation, banner: banners/dev-docs.webp }
|
|
859
963
|
```
|
|
@@ -863,7 +967,7 @@ site:
|
|
|
863
967
|
| `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below. |
|
|
864
968
|
| `base` | Where the package is served. Defaults to `/<contentPackage>/`. |
|
|
865
969
|
| `packages` | Which content packages this site renders. Defaults to its own. |
|
|
866
|
-
| `sections` |
|
|
970
|
+
| `sections` | What each section says about itself on its landing — see below. |
|
|
867
971
|
| `readmeSections` | The same, for a section whose landing comes from a `README`. |
|
|
868
972
|
| `landing` | Frontmatter for the mount's own `_index.md`. Passed through — the vocabulary is the theme's. |
|
|
869
973
|
| `backfillSections` | Write a bare `_index.md` for any other section directly under the mount. |
|
|
@@ -871,6 +975,42 @@ site:
|
|
|
871
975
|
| `pass` | A named bundle of this repository's own body rewrites. |
|
|
872
976
|
| `passOptions` | That bundle's options. |
|
|
873
977
|
|
|
978
|
+
### What a section may declare
|
|
979
|
+
|
|
980
|
+
A generated section landing is the **only** place a section can describe itself.
|
|
981
|
+
A content package authors no `_index.md` for `weapongear` or `affliction`, so
|
|
982
|
+
the file the theme reads is the one this build writes from `sections` — and
|
|
983
|
+
whatever that entry may carry is the whole of what the section can say.
|
|
984
|
+
|
|
985
|
+
| Key | Required | What it does |
|
|
986
|
+
| ------------- | -------- | ------------------------------------------------------------------------- |
|
|
987
|
+
| `title` | yes | The landing's heading, so it matches the card that links to it. |
|
|
988
|
+
| `banner` | no | The hero image, resolved as a CDN asset like any other `banner:`. |
|
|
989
|
+
| `description` | no | The hero standfirst under the heading, and the blurb a landing card uses. |
|
|
990
|
+
|
|
991
|
+
`readmeSections` takes the same three, for a section whose landing comes from a
|
|
992
|
+
`README` rather than from nothing. What the section declares wins over what the
|
|
993
|
+
`README` happens to carry — the landing has to match the card that links to it.
|
|
994
|
+
|
|
995
|
+
**The vocabulary is closed, and a key outside it is refused by name:**
|
|
996
|
+
|
|
997
|
+
```text
|
|
998
|
+
package-build config: `site.sections.affliction.descrption` is not a
|
|
999
|
+
recognized option (expected one of: title, banner, description).
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
That refusal is the point. `landing` is passed through unvalidated because it is
|
|
1003
|
+
written once, for the mount, in one landing template's own vocabulary. A section
|
|
1004
|
+
entry is written fourteen to twenty times per build against a contract every
|
|
1005
|
+
package and every section shares, so an unbounded one would let a mistyped
|
|
1006
|
+
`descrption:` publish into front matter, be read by nobody, and say nothing to
|
|
1007
|
+
anyone. Refusing it costs one line here when the vocabulary genuinely grows, and
|
|
1008
|
+
buys a build that cannot quietly emit a key no theme reads.
|
|
1009
|
+
|
|
1010
|
+
The **writers** name no keys: a section's `_index.md` is whatever the entry
|
|
1011
|
+
resolved to, `title` first. So extending the vocabulary is a change to the
|
|
1012
|
+
schema alone, and the two can no longer drift apart.
|
|
1013
|
+
|
|
874
1014
|
### Why `out` is required
|
|
875
1015
|
|
|
876
1016
|
The output tree is a build artifact and is **deleted on every run**, so that a
|
|
@@ -931,6 +1071,72 @@ None of them exits the process from inside the library; the command decides. Tha
|
|
|
931
1071
|
is what makes them testable, which the consumer scripts' inline `process.exit`
|
|
932
1072
|
calls were not.
|
|
933
1073
|
|
|
1074
|
+
## Diffing published addresses
|
|
1075
|
+
|
|
1076
|
+
```bash
|
|
1077
|
+
gh release download v0.8.2 -p system.zip -D build/baseline
|
|
1078
|
+
npx content-build addresses diff --from build/baseline/system.zip
|
|
1079
|
+
npx content-build addresses diff --from build/baseline/system.zip --strict
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
A package's `(type, shortcode)` addresses are a **published interface**. Every
|
|
1083
|
+
satellite declaring `itemCatalog: true` assembles its beings out of them —
|
|
1084
|
+
`attribute:str`, `skill:awar`, `weapongear:Tabri` — resolving each against the
|
|
1085
|
+
Item packs of the release its `compatibility.verified` pins. Renaming a
|
|
1086
|
+
shortcode is therefore a breaking change to something other repositories
|
|
1087
|
+
consume, and it used to cost nothing and produce no signal: the check that got
|
|
1088
|
+
made was a repository-local grep, which cannot see the other repositories and
|
|
1089
|
+
reports the reassuring answer.
|
|
1090
|
+
|
|
1091
|
+
`sohl` renamed one weapon's shortcode from `Tabri` to `Taburi` two days after
|
|
1092
|
+
the `v0.8.2` tag, on the stated ground that "nothing referenced the old value".
|
|
1093
|
+
True of that repository. Both satellites pin `v0.8.2` and address
|
|
1094
|
+
`weapongear:Tabri` on their copy of the same character — five lookups that
|
|
1095
|
+
resolve today and fail the moment either pin moves, reporting a missing item.
|
|
1096
|
+
|
|
1097
|
+
**A rename is told from a removal by the document id, and that is an identity
|
|
1098
|
+
match rather than an inference.** A note authors its `_id` in frontmatter; it is
|
|
1099
|
+
not derived from the shortcode, so it survives a rename. An address that
|
|
1100
|
+
disappeared while its document is still published elsewhere _is_ a rename:
|
|
1101
|
+
|
|
1102
|
+
```text
|
|
1103
|
+
assets/content/Weapons/Melee/Taburi.md:12:1: warning: since sohl@0.8.2, weapongear:Tabri is no longer published; the same document (s5D6QJbw7ZbETxdN) is now published as weapongear:Taburi. Every package that resolves weapongear:Tabri breaks when it moves past sohl@0.8.2
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Where the id is published under no address at all, that is all it says —
|
|
1107
|
+
**withdrawn**, with no successor named. A split, a deletion and a merge are
|
|
1108
|
+
indistinguishable at that point, and a "did you mean" guessed from string
|
|
1109
|
+
similarity would be worse than silence, because a wrong one sends the reader to
|
|
1110
|
+
the wrong fix.
|
|
1111
|
+
|
|
1112
|
+
| Finding | What it means | Severity |
|
|
1113
|
+
| ----------- | -------------------------------------------------------- | --------------------------------------- |
|
|
1114
|
+
| `renamed` | Address gone; the same document publishes under another. | `warning` — legitimate, but not silent |
|
|
1115
|
+
| `withdrawn` | Address gone; its document publishes under none. | `warning` — retiring content is allowed |
|
|
1116
|
+
|
|
1117
|
+
Neither fails a build. Retiring content is legitimate, and so is renaming — the
|
|
1118
|
+
shortcode charset rule forces some. What a rename must not do is happen without
|
|
1119
|
+
anyone noticing. `--strict` reports both as errors and exits non-zero, for a
|
|
1120
|
+
release workflow that wants a gate.
|
|
1121
|
+
|
|
1122
|
+
**A finding is placed against the note, not the pack it was read from.** The
|
|
1123
|
+
address space is read from compiled output because that is what actually ships;
|
|
1124
|
+
the content tree is read only to find the note carrying the id, so the reader
|
|
1125
|
+
lands on the `shortcode:` line they just edited. A withdrawal has no such note,
|
|
1126
|
+
so it degrades to the baseline document — and where neither is readable the
|
|
1127
|
+
position is dropped rather than guessed.
|
|
1128
|
+
|
|
1129
|
+
**The baseline is named, never derived, and never downloaded.** `--from` takes
|
|
1130
|
+
the artifact for the same reason `deps fetch --from` does: a command that
|
|
1131
|
+
reaches the network on its own is not reproducible and fails strangely offline.
|
|
1132
|
+
A baseline that yields **no** addressable item is refused rather than read as
|
|
1133
|
+
"nothing changed" — it would report a clean result for every possible input,
|
|
1134
|
+
which is the one failure a check like this can never catch.
|
|
1135
|
+
|
|
1136
|
+
Item packs only, because that is the address space consumers resolve against: a
|
|
1137
|
+
being's embedded items are the only cross-package resolution by
|
|
1138
|
+
`(type, shortcode)`.
|
|
1139
|
+
|
|
934
1140
|
## Diagnostics
|
|
935
1141
|
|
|
936
1142
|
Every warning or error a build reports **about a content note** is emitted in the
|
|
@@ -956,6 +1162,24 @@ Two rules keep it that way, both in `engine/diagnostics.mjs`:
|
|
|
956
1162
|
meaningless, `file: …` when only the note is known. Nothing defaults to
|
|
957
1163
|
`1:1`, which would send a reader to the frontmatter every time.
|
|
958
1164
|
|
|
1165
|
+
**A configuration error is located the same way.** Every check in
|
|
1166
|
+
`content-config.mjs` and `config.mjs` reports through one `fail()`, naming the
|
|
1167
|
+
offending key's dotted path — a good description and a bad locator, in a file
|
|
1168
|
+
that runs to hundreds of lines with sibling entries flow-mapped onto one. The
|
|
1169
|
+
path now rides on the error, and the loader that read the file resolves it
|
|
1170
|
+
against the YAML, so all of them come out located:
|
|
1171
|
+
|
|
1172
|
+
```text
|
|
1173
|
+
package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner).
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
The same two rules apply. A key the file never declares — a required one that is
|
|
1177
|
+
simply missing — has no node of its own, so the position names the **mapping it
|
|
1178
|
+
belongs in** and no further out; a missing _top-level_ key has nothing above it
|
|
1179
|
+
but the document, and an `.mjs` configuration has no YAML to resolve a path
|
|
1180
|
+
against at all. Both report `package-build.config.yaml: error: …`, the file
|
|
1181
|
+
without a line, rather than a line that would be wrong.
|
|
1182
|
+
|
|
959
1183
|
Establishing a position at all takes three corrections, applied only where they
|
|
960
1184
|
hold — see `positionInBody`. A body offset is not a file line until the
|
|
961
1185
|
frontmatter's lines are added (`bodyLine`); the trim that strips the body can
|
package/bin/content-build.mjs
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* npx content-build manifest [root] [--out <dir>]
|
|
46
46
|
* npx content-build site [--out <dir>]
|
|
47
47
|
* npx content-build reachability <dir> [file] [--index <shortcode>]
|
|
48
|
+
* npx content-build addresses diff --from <zip|dir> [--strict]
|
|
48
49
|
*
|
|
49
50
|
* In a consuming repository, wrapped as npm scripts — SoHL spells them:
|
|
50
51
|
* npm run build:compiledb // → … package compile (all packs)
|
|
@@ -92,6 +93,14 @@ import {
|
|
|
92
93
|
walkReachability,
|
|
93
94
|
} from "../engine/content-links.mjs";
|
|
94
95
|
import { emitDiagnostic, positionOfLiteral } from "../engine/diagnostics.mjs";
|
|
96
|
+
import {
|
|
97
|
+
readItemAddresses,
|
|
98
|
+
diffItemAddresses,
|
|
99
|
+
noteFilesById,
|
|
100
|
+
locateAddressFinding,
|
|
101
|
+
addressFindingMessage,
|
|
102
|
+
} from "../engine/address-diff.mjs";
|
|
103
|
+
import { itemPackJsonDirs } from "../engine/generate.mjs";
|
|
95
104
|
import {
|
|
96
105
|
formatUnaddressableFinding,
|
|
97
106
|
unaddressableForeignPackages,
|
|
@@ -147,6 +156,25 @@ prefix.apply(log, {
|
|
|
147
156
|
},
|
|
148
157
|
});
|
|
149
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Report a command's failure.
|
|
161
|
+
*
|
|
162
|
+
* A configuration error carries its own `file:line:column: error: ` locator
|
|
163
|
+
* (#95), and `loglevel`'s `[timestamp] [ERROR]:` prefix occupies exactly the
|
|
164
|
+
* position a parser reads the path from — so a located failure is printed
|
|
165
|
+
* unprefixed, as `emitDiagnostic` prints every other finding. Everything else
|
|
166
|
+
* is ordinary prose and keeps the log line it always had.
|
|
167
|
+
*
|
|
168
|
+
* @param {unknown} err - What was thrown.
|
|
169
|
+
* @returns {void}
|
|
170
|
+
*/
|
|
171
|
+
function reportFailure(err) {
|
|
172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
173
|
+
if (/** @type {{located?: boolean}} */ (err)?.located)
|
|
174
|
+
console.error(message);
|
|
175
|
+
else log.error(message);
|
|
176
|
+
}
|
|
177
|
+
|
|
150
178
|
const argv = yargs(hideBin(process.argv))
|
|
151
179
|
.command(packageCommand())
|
|
152
180
|
.command(depsCommand())
|
|
@@ -158,6 +186,7 @@ const argv = yargs(hideBin(process.argv))
|
|
|
158
186
|
.command(manifestCommand())
|
|
159
187
|
.command(siteCommand())
|
|
160
188
|
.command(reachabilityCommand())
|
|
189
|
+
.command(addressesCommand())
|
|
161
190
|
.version(ownVersion())
|
|
162
191
|
.help()
|
|
163
192
|
.alias("help", "h")
|
|
@@ -293,7 +322,7 @@ function docsCommand() {
|
|
|
293
322
|
process.stdout.write(page);
|
|
294
323
|
}
|
|
295
324
|
} catch (err) {
|
|
296
|
-
|
|
325
|
+
reportFailure(err);
|
|
297
326
|
process.exitCode = 1;
|
|
298
327
|
}
|
|
299
328
|
},
|
|
@@ -340,7 +369,11 @@ function lintCommand() {
|
|
|
340
369
|
const root = argv.root ?? config.paths.content;
|
|
341
370
|
const manifestDir = argv.manifests ?? config.paths.manifests;
|
|
342
371
|
|
|
343
|
-
|
|
372
|
+
// The package is passed for the homepage rule (#52), which
|
|
373
|
+
// names the address a tree with no front page fails to serve.
|
|
374
|
+
const addresses = lintContentTree(root, {
|
|
375
|
+
contentPackage: config.contentPackage,
|
|
376
|
+
});
|
|
344
377
|
// One index, built once, for the reference check. It is the
|
|
345
378
|
// same resolver the wikilink audit uses, so a frontmatter
|
|
346
379
|
// reference and a body link answer the same way.
|
|
@@ -371,7 +404,7 @@ function lintCommand() {
|
|
|
371
404
|
);
|
|
372
405
|
}
|
|
373
406
|
} catch (err) {
|
|
374
|
-
|
|
407
|
+
reportFailure(err);
|
|
375
408
|
process.exitCode = 1;
|
|
376
409
|
}
|
|
377
410
|
},
|
|
@@ -452,7 +485,7 @@ function formatCommand() {
|
|
|
452
485
|
log.info(`Formatting is clean (${checked} file(s)).`);
|
|
453
486
|
}
|
|
454
487
|
} catch (err) {
|
|
455
|
-
|
|
488
|
+
reportFailure(err);
|
|
456
489
|
process.exitCode = 1;
|
|
457
490
|
}
|
|
458
491
|
},
|
|
@@ -501,7 +534,7 @@ function markdownCommand() {
|
|
|
501
534
|
log.info("Markdown is clean.");
|
|
502
535
|
}
|
|
503
536
|
} catch (err) {
|
|
504
|
-
|
|
537
|
+
reportFailure(err);
|
|
505
538
|
process.exitCode = 1;
|
|
506
539
|
}
|
|
507
540
|
},
|
|
@@ -649,7 +682,7 @@ function linksCommand() {
|
|
|
649
682
|
);
|
|
650
683
|
}
|
|
651
684
|
} catch (err) {
|
|
652
|
-
|
|
685
|
+
reportFailure(err);
|
|
653
686
|
process.exitCode = 1;
|
|
654
687
|
}
|
|
655
688
|
},
|
|
@@ -725,7 +758,7 @@ function manifestCommand() {
|
|
|
725
758
|
});
|
|
726
759
|
}
|
|
727
760
|
} catch (err) {
|
|
728
|
-
|
|
761
|
+
reportFailure(err);
|
|
729
762
|
process.exitCode = 1;
|
|
730
763
|
}
|
|
731
764
|
},
|
|
@@ -767,6 +800,11 @@ function siteCommand() {
|
|
|
767
800
|
});
|
|
768
801
|
const { gates } = result;
|
|
769
802
|
|
|
803
|
+
// First, because it is decided before the tree is walked and
|
|
804
|
+
// before the output is cleared: a package with no front page,
|
|
805
|
+
// or two competing for it, has nothing to say about its pages
|
|
806
|
+
// yet (#52).
|
|
807
|
+
for (const f of gates.homepages) emitDiagnostic(f);
|
|
770
808
|
for (const f of gates.frontmatterLinks) {
|
|
771
809
|
emitDiagnostic({
|
|
772
810
|
file: f.file,
|
|
@@ -849,7 +887,7 @@ function siteCommand() {
|
|
|
849
887
|
`landing(s) to ${path.relative(process.cwd(), s.out)}`,
|
|
850
888
|
);
|
|
851
889
|
} catch (err) {
|
|
852
|
-
|
|
890
|
+
reportFailure(err);
|
|
853
891
|
process.exitCode = 1;
|
|
854
892
|
}
|
|
855
893
|
},
|
|
@@ -943,7 +981,7 @@ function reachabilityCommand() {
|
|
|
943
981
|
);
|
|
944
982
|
}
|
|
945
983
|
} catch (err) {
|
|
946
|
-
|
|
984
|
+
reportFailure(err);
|
|
947
985
|
process.exitCode = 1;
|
|
948
986
|
}
|
|
949
987
|
},
|
|
@@ -1032,6 +1070,154 @@ function depsCommand() {
|
|
|
1032
1070
|
const count = await fetchAllCatalogs(config);
|
|
1033
1071
|
if (count)
|
|
1034
1072
|
log.info(`Fetched ${count} dependency catalogue(s).`);
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
reportFailure(err);
|
|
1075
|
+
process.exitCode = 1;
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* `addresses diff` — report every published `(type, shortcode)` this build no
|
|
1083
|
+
* longer publishes, against a released artifact.
|
|
1084
|
+
*
|
|
1085
|
+
* The address space is a published interface (see `engine/address-diff.mjs`),
|
|
1086
|
+
* and renaming a shortcode used to cost nothing and produce no signal. This is
|
|
1087
|
+
* the signal, emitted in the repository doing the renaming while the change is
|
|
1088
|
+
* still in front of the author.
|
|
1089
|
+
*
|
|
1090
|
+
* **Its own command rather than a step of `package compile`.** It reads a
|
|
1091
|
+
* *second* artifact that the compile knows nothing about and that has to be
|
|
1092
|
+
* obtained separately, and it is a question about a release rather than about a
|
|
1093
|
+
* build — a repository between releases has nothing to compare against.
|
|
1094
|
+
*
|
|
1095
|
+
* **The baseline is named, never derived, and never downloaded.** `--from`
|
|
1096
|
+
* takes the artifact — the `.zip` a release publishes, or a directory built
|
|
1097
|
+
* from one — for the same reason `deps fetch --from` does: a command that
|
|
1098
|
+
* reaches the network on its own is not reproducible and fails strangely
|
|
1099
|
+
* offline. In a release workflow the artifact is one line ahead of it:
|
|
1100
|
+
*
|
|
1101
|
+
* ```sh
|
|
1102
|
+
* gh release download v0.8.2 -p system.zip -D build/baseline
|
|
1103
|
+
* npx content-build addresses diff --from build/baseline/system.zip
|
|
1104
|
+
* ```
|
|
1105
|
+
*
|
|
1106
|
+
* @param {object} config - The resolved build configuration.
|
|
1107
|
+
* @param {{from: string, strict?: boolean}} argv - The parsed arguments.
|
|
1108
|
+
* @returns {Promise<void>}
|
|
1109
|
+
*/
|
|
1110
|
+
async function diffAddresses(config, argv) {
|
|
1111
|
+
// The baseline is this package's own earlier self, so it is cached beside
|
|
1112
|
+
// the dependency catalogues rather than among them — a release of `sohl`
|
|
1113
|
+
// is not a dependency of `sohl`, and filing it as one would collide with a
|
|
1114
|
+
// genuine relationship of the same id.
|
|
1115
|
+
const cacheConfig = {
|
|
1116
|
+
...config,
|
|
1117
|
+
paths: {
|
|
1118
|
+
...config.paths,
|
|
1119
|
+
foreignCache: path.join(
|
|
1120
|
+
path.dirname(config.paths.foreignCache),
|
|
1121
|
+
"baseline",
|
|
1122
|
+
),
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
const dir = await fetchCatalogFromPath(
|
|
1126
|
+
cacheConfig,
|
|
1127
|
+
{ id: config.foundryPackage },
|
|
1128
|
+
argv.from,
|
|
1129
|
+
);
|
|
1130
|
+
// `<id>@<version>`, which is what the diagnostics name the baseline by.
|
|
1131
|
+
const label = path.basename(dir);
|
|
1132
|
+
|
|
1133
|
+
const itemsRoot = path.join(dir, "items");
|
|
1134
|
+
const baselineDirs = fs
|
|
1135
|
+
.readdirSync(itemsRoot)
|
|
1136
|
+
.map((name) => path.join(itemsRoot, name));
|
|
1137
|
+
const currentDirs = itemPackJsonDirs(config);
|
|
1138
|
+
if (!currentDirs.length) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
'this repository declares no pack of type "Item", so it ' +
|
|
1141
|
+
"publishes no item addresses to diff",
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const findings = diffItemAddresses(
|
|
1146
|
+
readItemAddresses(baselineDirs),
|
|
1147
|
+
readItemAddresses(currentDirs),
|
|
1148
|
+
{ baseline: label },
|
|
1149
|
+
);
|
|
1150
|
+
if (!findings.length) {
|
|
1151
|
+
log.info(`Every address ${label} published is still published.`);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// A rename is fixed in the note that made it, so findings are placed
|
|
1156
|
+
// against the tree rather than against the compiled output they were read
|
|
1157
|
+
// from.
|
|
1158
|
+
const noteFiles = noteFilesById(config.paths.content);
|
|
1159
|
+
const severity = argv.strict ? "error" : "warning";
|
|
1160
|
+
for (const finding of findings) {
|
|
1161
|
+
emitDiagnostic({
|
|
1162
|
+
...locateAddressFinding(finding, noteFiles),
|
|
1163
|
+
severity,
|
|
1164
|
+
message: addressFindingMessage(finding),
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
const renamed = findings.filter((f) => f.kind === "renamed").length;
|
|
1168
|
+
log.info(
|
|
1169
|
+
`${findings.length} address(es) ${label} published are no longer ` +
|
|
1170
|
+
`published (${renamed} renamed, ${findings.length - renamed} ` +
|
|
1171
|
+
`withdrawn).`,
|
|
1172
|
+
);
|
|
1173
|
+
// Retiring content is legitimate and so is renaming; neither fails a build
|
|
1174
|
+
// unless the caller asked for a gate.
|
|
1175
|
+
if (argv.strict) process.exitCode = 1;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function addressesCommand() {
|
|
1179
|
+
return {
|
|
1180
|
+
command: "addresses <action>",
|
|
1181
|
+
describe:
|
|
1182
|
+
"Compare the addresses this build publishes against a release's",
|
|
1183
|
+
builder: (yargs) => {
|
|
1184
|
+
// Required, for the reason every other action is (#57): an
|
|
1185
|
+
// optional one exits 0 having compared nothing.
|
|
1186
|
+
yargs.positional("action", {
|
|
1187
|
+
describe: "The action to perform.",
|
|
1188
|
+
type: "string",
|
|
1189
|
+
choices: ["diff"],
|
|
1190
|
+
});
|
|
1191
|
+
yargs.option("from", {
|
|
1192
|
+
describe:
|
|
1193
|
+
"The released artifact to compare against — a package " +
|
|
1194
|
+
"zip, or the directory it was built from.",
|
|
1195
|
+
type: "string",
|
|
1196
|
+
});
|
|
1197
|
+
yargs.option("strict", {
|
|
1198
|
+
describe:
|
|
1199
|
+
"Report findings as errors and exit non-zero, for a " +
|
|
1200
|
+
"release workflow that gates on them.",
|
|
1201
|
+
type: "boolean",
|
|
1202
|
+
default: false,
|
|
1203
|
+
});
|
|
1204
|
+
// Checked while parsing, so it fails in a repository whose
|
|
1205
|
+
// configuration a handler would never get far enough to resolve.
|
|
1206
|
+
yargs.check((parsed) => {
|
|
1207
|
+
if (parsed.action === "diff" && !parsed.from) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
"`addresses diff` needs `--from <zip|dir>`: the " +
|
|
1210
|
+
"release to compare against. There is nothing to " +
|
|
1211
|
+
"derive it from — a repository between releases " +
|
|
1212
|
+
"has no previous artifact on disk.",
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
return true;
|
|
1216
|
+
});
|
|
1217
|
+
},
|
|
1218
|
+
handler: async (argv) => {
|
|
1219
|
+
try {
|
|
1220
|
+
await diffAddresses(loadPackConfig(), argv);
|
|
1035
1221
|
} catch (err) {
|
|
1036
1222
|
log.error(err.message);
|
|
1037
1223
|
process.exitCode = 1;
|
|
@@ -1095,7 +1281,7 @@ function packageCommand() {
|
|
|
1095
1281
|
});
|
|
1096
1282
|
}
|
|
1097
1283
|
} catch (err) {
|
|
1098
|
-
|
|
1284
|
+
reportFailure(err);
|
|
1099
1285
|
process.exitCode = 1;
|
|
1100
1286
|
}
|
|
1101
1287
|
},
|
package/bin/package-build.mjs
CHANGED
|
@@ -135,7 +135,14 @@ function ownVersion() {
|
|
|
135
135
|
*/
|
|
136
136
|
function die(err) {
|
|
137
137
|
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
-
|
|
138
|
+
// A located diagnostic already starts with `file:line:column:`, which is
|
|
139
|
+
// exactly the position a parser reads the path from — prefixing it would
|
|
140
|
+
// yield a filename no editor can open (#95).
|
|
141
|
+
console.error(
|
|
142
|
+
/** @type {{located?: boolean}} */ (err)?.located ? message : (
|
|
143
|
+
`package-build: ${message}`
|
|
144
|
+
),
|
|
145
|
+
);
|
|
139
146
|
process.exit(1);
|
|
140
147
|
}
|
|
141
148
|
|