@heroiclands/package-build 20.4.0 → 20.6.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 +288 -0
- package/CONTENT.md +213 -20
- package/README.md +19 -1
- package/bin/content-build.mjs +135 -32
- package/bin/package-build.mjs +46 -13
- package/content-config.mjs +345 -101
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +16 -6
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/engine/actor-compiler.mjs +30 -27
- package/engine/address-diff.mjs +45 -41
- package/engine/base-compiler.mjs +6 -0
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +9 -9
- package/engine/content-index.mjs +44 -23
- package/engine/content-links.mjs +44 -11
- package/engine/content-lint.mjs +44 -10
- package/engine/content-tables.mjs +32 -27
- package/engine/folder-notes.mjs +4 -2
- package/engine/frontmatter-lint.mjs +35 -38
- package/engine/generate.mjs +5 -0
- package/engine/helpers.mjs +86 -32
- package/engine/index.mjs +12 -2
- package/engine/journals.mjs +9 -0
- package/engine/note-claims.mjs +18 -10
- package/engine/note-schemas.mjs +0 -5
- package/engine/note-vocabulary.mjs +32 -31
- package/engine/pack-config.mjs +26 -12
- package/engine/pack-router.mjs +0 -0
- package/engine/pdf-build.mjs +464 -0
- package/engine/pdf-fonts.mjs +420 -0
- package/engine/pdf-render.mjs +876 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/scenes.mjs +14 -5
- package/engine/schema-check.mjs +1 -1
- package/engine/site-build.mjs +21 -3
- package/engine/web-wikilinks.mjs +6 -3
- package/engine/wikilinks.mjs +2 -4
- package/hm3/actors.mjs +8 -0
- package/hm3/items.mjs +8 -0
- package/package.json +1 -1
- package/release.mjs +63 -3
- package/sohl/actors.mjs +8 -0
- package/sohl/items.mjs +8 -0
- package/sohl/note-schemas.mjs +5 -5
- package/types/content-config.d.mts +66 -15
- package/types/engine/actor-compiler.d.mts +34 -30
- package/types/engine/address-diff.d.mts +57 -3
- package/types/engine/base-compiler.d.mts +10 -2
- package/types/engine/bundles.d.mts +9 -0
- package/types/engine/content-address.d.mts +9 -9
- package/types/engine/content-index.d.mts +57 -13
- package/types/engine/content-lint.d.mts +6 -4
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/frontmatter-lint.d.mts +3 -2
- package/types/engine/helpers.d.mts +105 -31
- package/types/engine/index.d.mts +4 -0
- package/types/engine/journals.d.mts +9 -0
- package/types/engine/note-claims.d.mts +17 -10
- package/types/engine/note-vocabulary.d.mts +23 -196
- package/types/engine/pack-config.d.mts +4 -4
- package/types/engine/pdf-build.d.mts +42 -0
- package/types/engine/pdf-fonts.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +156 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +2 -2
- package/types/engine/site-build.d.mts +34 -6
- package/types/engine/wikilinks.d.mts +2 -3
- package/types/hm3/actors.d.mts +8 -0
- package/types/hm3/items.d.mts +8 -0
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +10 -2
- package/types/sohl/items.d.mts +8 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# Project setup
|
|
2
|
+
|
|
3
|
+
[`getting-started.md`](getting-started.md) builds a package. This document
|
|
4
|
+
describes the repository around it: the files every HeroicLands package carries
|
|
5
|
+
beyond `package-build.config.yaml`, what each npm script in the chain is for,
|
|
6
|
+
and the directory layout the build reads and writes.
|
|
7
|
+
|
|
8
|
+
None of it is required to compile a pack. All of it is required for a repository
|
|
9
|
+
someone else can contribute to, and the reason to write it down is that
|
|
10
|
+
otherwise each repository copies it from the last and the copies drift.
|
|
11
|
+
|
|
12
|
+
## The layout
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
acme-bestiary/
|
|
16
|
+
├── package.json # identity, and every script below
|
|
17
|
+
├── package-lock.json # committed
|
|
18
|
+
├── package-build.config.yaml # the build's single source
|
|
19
|
+
├── prettier.config.js # re-exports the shared Prettier options
|
|
20
|
+
├── .prettierignore # committed files Prettier must not rewrite
|
|
21
|
+
├── .gitignore # what is generated
|
|
22
|
+
├── .changeset/
|
|
23
|
+
│ ├── config.json
|
|
24
|
+
│ └── *.md # one per unreleased change
|
|
25
|
+
├── .github/
|
|
26
|
+
│ ├── labels.yml # the closed label registry
|
|
27
|
+
│ ├── ISSUE_REPORTING.md # §3 lists the same labels
|
|
28
|
+
│ └── workflows/
|
|
29
|
+
├── assets/
|
|
30
|
+
│ └── content/ # authored notes — an Obsidian vault
|
|
31
|
+
├── lang/
|
|
32
|
+
│ └── en.json
|
|
33
|
+
├── src/ # module JavaScript, if the package has any
|
|
34
|
+
├── styles/
|
|
35
|
+
├── README.md
|
|
36
|
+
├── LICENSE.md
|
|
37
|
+
└── build/ # everything generated; gitignored
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`build/` is the only directory the toolchain writes into, and every path under
|
|
41
|
+
it is configurable through [`paths`](configuration.md#paths). The defaults:
|
|
42
|
+
|
|
43
|
+
| Path | Written by | Holds |
|
|
44
|
+
| ----------------------- | ------------------------------ | ------------------------------------------------------------- |
|
|
45
|
+
| `build/packs-json/` | `content-build package` | One JSON file per compiled document, before the pack is made. |
|
|
46
|
+
| `build/stage/packs/` | `content-build package` | The LevelDB compendium packs. |
|
|
47
|
+
| `build/stage/` | `package-build assets` | The staged package — manifest, packs, and the shipped files. |
|
|
48
|
+
| `build/content-index/` | `content-build content-index` | This package's published note index. |
|
|
49
|
+
| `build/cache/foreign/` | `content-build deps` | A dependency's unpacked item catalogue. |
|
|
50
|
+
| `build/cache/metadata/` | `content-build deps` | A dependency's fetched content index. |
|
|
51
|
+
| `build/tmp/packs/` | `content-build package unpack` | A compiled pack extracted back to JSON. |
|
|
52
|
+
| `build/dist/` | `package-build release` | The release archive, and the book when one is built. |
|
|
53
|
+
|
|
54
|
+
The two that matter when something is wrong are `build/packs-json/`, which is
|
|
55
|
+
the compiled document before Foundry ever sees it, and `build/stage/`, which is
|
|
56
|
+
exactly what ships.
|
|
57
|
+
|
|
58
|
+
**`assets/content/` is opened as an Obsidian vault.** There is no export step
|
|
59
|
+
and no vault environment variable: the notes in the repository are the source,
|
|
60
|
+
and a plain checkout builds. Dataview renders the content tables live while
|
|
61
|
+
authoring, which is why the tree is arranged for humans even though `type:`
|
|
62
|
+
rather than location is what routes a note.
|
|
63
|
+
|
|
64
|
+
## `package.json`
|
|
65
|
+
|
|
66
|
+
Beyond the identity keys [`getting-started.md`](getting-started.md) covers,
|
|
67
|
+
`package.json` carries the whole command surface of the repository. Nobody types
|
|
68
|
+
`npx content-build package compile`; they type `npm run build`.
|
|
69
|
+
|
|
70
|
+
The scripts fall into six groups, and the grouping is the point — a contributor
|
|
71
|
+
who knows the groups can guess a name.
|
|
72
|
+
|
|
73
|
+
### `prepare` — the git hooks
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
"prepare": "git config core.hooksPath node_modules/@heroiclands/package-build/githooks || true"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
npm runs `prepare` after every install, so this is what installs the hooks. It
|
|
80
|
+
points git at the hook directory this package ships, which means a repository
|
|
81
|
+
carries no hook files of its own and cannot drift from the others. `|| true` so
|
|
82
|
+
that an install outside a git checkout — a Docker build, a CI cache step —
|
|
83
|
+
does not fail on it.
|
|
84
|
+
|
|
85
|
+
Three hooks arrive with it, each with its own switch, read with git's normal
|
|
86
|
+
precedence: a plain `git config` sets one clone, `--global` sets a machine.
|
|
87
|
+
|
|
88
|
+
| Hook | What it does | Key | Default |
|
|
89
|
+
| -------------------------------- | --------------------------------------------------------------- | ------------------------------------ | ------- |
|
|
90
|
+
| `pre-commit`, `pre-merge-commit` | Refuse a commit on a protected branch. | `hooks.allowCommitOnMain` (inverted) | on |
|
|
91
|
+
| `commit-msg` | Refuse AI attribution in a commit message. | `hooks.noAttribution` | on |
|
|
92
|
+
| `pre-push` | Run this repository's own Build & Test workflow in a container. | `hooks.prePushCi` | off |
|
|
93
|
+
|
|
94
|
+
The defaults differ deliberately: a guard that costs nothing is on unless
|
|
95
|
+
refused, and one that runs a container for minutes is off unless asked for.
|
|
96
|
+
|
|
97
|
+
The branch guard is the one you meet first:
|
|
98
|
+
|
|
99
|
+
```console
|
|
100
|
+
$ git commit -m "Add the bestiary content package"
|
|
101
|
+
pre-commit: refusing to commit on the protected branch 'master'.
|
|
102
|
+
|
|
103
|
+
'master' is protected on GitHub, so this commit could never be pushed from
|
|
104
|
+
here. Move it onto a branch first — this keeps everything you have staged:
|
|
105
|
+
|
|
106
|
+
git switch -c <type>/<issue_#>_<slug>
|
|
107
|
+
|
|
108
|
+
To commit here anyway just this once, use 'git commit --no-verify'. To opt this
|
|
109
|
+
repository out permanently, 'git config hooks.allowCommitOnMain true'.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`pre-push` reads the step list out of `.github/workflows/build.yml` rather than
|
|
113
|
+
holding its own copy, and runs it over a clean export of `HEAD` in a
|
|
114
|
+
`linux/amd64` container. That last detail is the one a Mac cannot reproduce any
|
|
115
|
+
other way: this filesystem is case-insensitive and the runner's is not, so a
|
|
116
|
+
wrong-case import passes locally and fails there.
|
|
117
|
+
|
|
118
|
+
### `clean` — removing what the build wrote
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
"clean": "package-build clean",
|
|
122
|
+
"distclean": "package-build clean --distclean"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`clean` removes the conventional build directories plus anything named in
|
|
126
|
+
`packageBuild.clean.extra` — a site's `content/`, `public/` and `resources/`,
|
|
127
|
+
say. `distclean` additionally removes `node_modules`. Both exit 0 whether or not
|
|
128
|
+
there was anything to remove.
|
|
129
|
+
|
|
130
|
+
### `lint:*` — the checks, one per question
|
|
131
|
+
|
|
132
|
+
Six checks, each answering one question and blind to what the others see. They
|
|
133
|
+
are separate scripts rather than one because a failing chain stops at its first
|
|
134
|
+
failure, and knowing _which_ question failed is most of the diagnosis.
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
"lint": "run-s lint:format lint:markdown lint:addresses lint:content-links lint:lang lint:labels",
|
|
138
|
+
"lint:format": "content-build format",
|
|
139
|
+
"lint:markdown": "content-build markdown",
|
|
140
|
+
"lint:markdown:fix": "content-build markdown --fix",
|
|
141
|
+
"lint:addresses": "content-build lint",
|
|
142
|
+
"lint:content-links": "content-build links",
|
|
143
|
+
"lint:lang": "package-build lang check",
|
|
144
|
+
"lint:labels": "package-build labels check"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- **`lint:format`** — is every file formatted to the shared Prettier options?
|
|
148
|
+
Reports without writing, which is what a CI gate wants; `npm run format`
|
|
149
|
+
is the writing form.
|
|
150
|
+
- **`lint:markdown`** — does the prose satisfy the shared markdownlint rule set?
|
|
151
|
+
`lint:markdown:fix` applies the fixes markdownlint can make.
|
|
152
|
+
- **`lint:addresses`** — is every note's frontmatter well-formed, is every
|
|
153
|
+
address unique, and does the tree hold exactly one homepage?
|
|
154
|
+
- **`lint:content-links`** — does every `[[…]]` resolve, does every `#anchor`
|
|
155
|
+
land, and is every cross-package reference reachable through a fetched
|
|
156
|
+
manifest?
|
|
157
|
+
- **`lint:lang`** — does every localization file survive
|
|
158
|
+
`foundry.utils.expandObject`? A dotted-prefix collision (`"a.b": 1` beside
|
|
159
|
+
`"a.b.c": 2`) makes Foundry drop the whole file silently, which is the quiet
|
|
160
|
+
failure this catches. A package with templates and scripts also wires
|
|
161
|
+
`package-build lang coverage` and `package-build lang hardcoded`; a
|
|
162
|
+
content-only package has nothing for those two to read, and each dies naming
|
|
163
|
+
the glob that matched nothing rather than passing vacuously.
|
|
164
|
+
- **`lint:labels`** — do `.github/labels.yml` and §3 of
|
|
165
|
+
`.github/ISSUE_REPORTING.md` still list the same labels? Neither derives from
|
|
166
|
+
the other, so nothing else notices when they drift.
|
|
167
|
+
|
|
168
|
+
`run-s` comes from `npm-run-all`, a devDependency. It runs scripts in sequence
|
|
169
|
+
and stops at the first failure, which is what makes a named chain readable.
|
|
170
|
+
|
|
171
|
+
### `build:*` — the chain
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
"build": "npm ci && npm run build:noci",
|
|
175
|
+
"build:local": "npm i && npm run build:noci",
|
|
176
|
+
"build:noci": "run-s lint build:db build:module",
|
|
177
|
+
"build:db": "run-s build:content-index build:assets build:compiledb",
|
|
178
|
+
"build:content-index": "content-build content-index",
|
|
179
|
+
"build:assets": "package-build assets",
|
|
180
|
+
"build:compiledb": "content-build package compile",
|
|
181
|
+
"build:unpackdb": "content-build package unpack",
|
|
182
|
+
"build:module": "package-build manifest",
|
|
183
|
+
"build:pack-release": "package-build release"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The split between `build` and `build:noci` is not cosmetic. **`build` installs
|
|
187
|
+
from the lockfile first**, so what it compiles is what CI compiles;
|
|
188
|
+
`build:local` installs without the lockfile, for a working tree mid-change; and
|
|
189
|
+
`build:noci` skips the install entirely, which is the one to reach for inside a
|
|
190
|
+
git worktree where `node_modules` is already correct.
|
|
191
|
+
|
|
192
|
+
Within the chain, two orderings are real:
|
|
193
|
+
|
|
194
|
+
- **`build:content-index` before `build:pack-release`.** The manifest advertises
|
|
195
|
+
the content index at a pinned address, and `package-build release` refuses an
|
|
196
|
+
archive whose manifest advertises an index the tree never wrote.
|
|
197
|
+
- **`lint` before any of it.** The checks read the tree; the compilers read the
|
|
198
|
+
tree and write. Finding a malformed address after the packs are built means
|
|
199
|
+
rebuilding them.
|
|
200
|
+
|
|
201
|
+
The three stage-writing steps — `build:assets`, `build:compiledb`,
|
|
202
|
+
`build:module` — each write their own part of `build/stage/` and none clobbers
|
|
203
|
+
another's, so their order among themselves is legibility rather than necessity.
|
|
204
|
+
|
|
205
|
+
`build:unpackdb` is the inverse of `build:compiledb`: it extracts a compiled
|
|
206
|
+
LevelDB pack back to JSON, which is how you read what actually shipped.
|
|
207
|
+
|
|
208
|
+
A package with a dependency adds `"build:deps": "content-build deps fetch"` at
|
|
209
|
+
the head of `build:db`. Fetching is its own step and never happens during a
|
|
210
|
+
compile, so a build never reaches the network silently — a cold cache fails
|
|
211
|
+
naming `deps fetch`.
|
|
212
|
+
|
|
213
|
+
### `format` — writing rather than checking
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
"format": "content-build format --write",
|
|
217
|
+
"format:check": "content-build format"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Two names for one command because two audiences want it: a contributor wants
|
|
221
|
+
the writing form, and CI wants the checking form. `format:check` and
|
|
222
|
+
`lint:format` are the same command; both names exist because the lint chain
|
|
223
|
+
reads better with one and a CI workflow reads better with the other.
|
|
224
|
+
|
|
225
|
+
### `changeset:*` — the release mechanism
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
"changeset": "changeset",
|
|
229
|
+
"changeset:check": "changeset status --since=origin/main",
|
|
230
|
+
"changeset:version": "changeset version && npm install --package-lock-only"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`changeset` adds one. `changeset:check` reports what is pending against `main`,
|
|
234
|
+
which is what a CI job gates on so that a behaviour change cannot merge without
|
|
235
|
+
declaring its bump. `changeset:version` consumes the pending changesets, writes
|
|
236
|
+
`CHANGELOG.md` and bumps `package.json` — and the `npm install --package-lock-only`
|
|
237
|
+
after it is what keeps `package-lock.json`'s recorded version in step, since
|
|
238
|
+
changesets does not touch the lockfile.
|
|
239
|
+
|
|
240
|
+
### The site scripts
|
|
241
|
+
|
|
242
|
+
A package that publishes a website adds a further group. They are outside this
|
|
243
|
+
document's scope because they need Hugo and the shared theme submodule, but the
|
|
244
|
+
shape is worth knowing so the names are not surprising:
|
|
245
|
+
`build:site-content` writes the Hugo content tree with `content-build site`,
|
|
246
|
+
`build:site-html` runs Hugo over it, and `serve:site` does the first and then
|
|
247
|
+
`hugo server` for a live preview.
|
|
248
|
+
|
|
249
|
+
### Deployment scripts
|
|
250
|
+
|
|
251
|
+
`push:dev`, `push:qa` and `push:prod` wrap `package-build deploy <stage>`, and
|
|
252
|
+
`deploy:*` chains a build in front of each. They read credentials from
|
|
253
|
+
`.env.local`, which is never committed. A fresh worktree has no `.env.local`, so
|
|
254
|
+
these exit without copying anything and the target keeps serving the previous
|
|
255
|
+
build — copy the file in before trusting a deployment from one.
|
|
256
|
+
|
|
257
|
+
## `prettier.config.js`
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
/**
|
|
261
|
+
* The shared HeroicLands Prettier configuration.
|
|
262
|
+
*
|
|
263
|
+
* @type {import("prettier").Config}
|
|
264
|
+
*/
|
|
265
|
+
export { default } from "@heroiclands/package-build/prettier";
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Never restate an option here.** The options live in
|
|
269
|
+
`@heroiclands/package-build/prettier` so that every repository formats
|
|
270
|
+
identically, and a local override is how two repositories stop agreeing. The
|
|
271
|
+
file exists solely so that an editor and a bare `npx prettier` resolve the same
|
|
272
|
+
options the toolchain applies — without it they fall back to Prettier's own
|
|
273
|
+
defaults, and a print width that differs from the project's turns every
|
|
274
|
+
save into a diff the lint chain reverts.
|
|
275
|
+
|
|
276
|
+
`content-build markdown` needs no equivalent file: it hands the shared rule set
|
|
277
|
+
to markdownlint directly, and a repository that declares nothing gets it. A
|
|
278
|
+
`.markdownlint-cli2.jsonc` is for the editor's markdownlint extension, and
|
|
279
|
+
[`@heroiclands/package-build/markdownlint`](api.md) is the export it re-uses —
|
|
280
|
+
with one trap worth knowing before writing one. A consumer file overrides the
|
|
281
|
+
shared configuration **key by key, and each key wholesale**: declaring only
|
|
282
|
+
`ignores` keeps every rule intact but _replaces_ the shared ignore list rather
|
|
283
|
+
than extending it, so such a file must restate every shared entry it still
|
|
284
|
+
wants.
|
|
285
|
+
|
|
286
|
+
## `.prettierignore`
|
|
287
|
+
|
|
288
|
+
`content-build format` already consults `.gitignore`, so generated trees named
|
|
289
|
+
there need no second entry. `.prettierignore` is for the opposite case: files
|
|
290
|
+
that are **committed** and must not be rewritten.
|
|
291
|
+
|
|
292
|
+
```text
|
|
293
|
+
# Written by `changeset version`, not by hand. The Version Packages pull request
|
|
294
|
+
# runs no CI — it is opened by GITHUB_TOKEN — so a changelog the formatter has
|
|
295
|
+
# rewritten turns `main` red the moment that pull request merges.
|
|
296
|
+
CHANGELOG.md
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Two more entries are common, and both are the same principle:
|
|
300
|
+
|
|
301
|
+
- **A generated site tree.** Hugo layouts are Go templates in `.html` files,
|
|
302
|
+
which Prettier reflows as HTML — folding the template directives into prose.
|
|
303
|
+
- **Vendored files from another build.** A file copied verbatim from another
|
|
304
|
+
repository is reformatted into a permanent diff against its source, which
|
|
305
|
+
re-diffs on every re-vendor.
|
|
306
|
+
|
|
307
|
+
`package-lock.json` needs no entry when `package.json` is formatted, because npm
|
|
308
|
+
matches `package.json`'s indentation when it writes the lockfile — so a
|
|
309
|
+
formatted `package.json` produces a lockfile the formatter already agrees with.
|
|
310
|
+
A repository whose `package.json` is indented some other way ignores the
|
|
311
|
+
lockfile instead, or `npm install` and the formatter rewrite it in turn.
|
|
312
|
+
|
|
313
|
+
## `.gitignore`
|
|
314
|
+
|
|
315
|
+
The build's own output, plus the local-only paths every repository has:
|
|
316
|
+
|
|
317
|
+
```text
|
|
318
|
+
node_modules
|
|
319
|
+
/build/
|
|
320
|
+
/nogit/
|
|
321
|
+
/.env
|
|
322
|
+
/.env.local
|
|
323
|
+
/.claude/
|
|
324
|
+
CLAUDE.md
|
|
325
|
+
AI.md
|
|
326
|
+
*.tgz
|
|
327
|
+
.DS_Store
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Three of those carry rules rather than conventions.
|
|
331
|
+
|
|
332
|
+
**`nogit/` is local scratch** — design write-ups, working notes, downloaded
|
|
333
|
+
references. Nothing in it is ever committed.
|
|
334
|
+
|
|
335
|
+
**`.env.local` holds real credentials** for the deployment targets. Never
|
|
336
|
+
committed, never echoed.
|
|
337
|
+
|
|
338
|
+
**`node_modules` with no leading slash and no trailing slash.** The trailing
|
|
339
|
+
slash matches a directory only, so a worktree carrying `node_modules` as a
|
|
340
|
+
symlink is not ignored and a `git add -A` commits a symlink holding one
|
|
341
|
+
developer's absolute path. Unanchored so the same cannot happen one directory
|
|
342
|
+
down, which is where a nested worktree puts it.
|
|
343
|
+
|
|
344
|
+
Note also that `.gitignore` is what keeps `build/` out of the prose checks:
|
|
345
|
+
`content-build format` reads `.gitignore` and `.prettierignore` both, and
|
|
346
|
+
`content-build markdown` reads `.gitignore`.
|
|
347
|
+
|
|
348
|
+
## `.changeset/`
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{
|
|
352
|
+
"$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json",
|
|
353
|
+
"changelog": "@changesets/changelog-git",
|
|
354
|
+
"commit": true,
|
|
355
|
+
"baseBranch": "main",
|
|
356
|
+
"privatePackages": {
|
|
357
|
+
"version": true,
|
|
358
|
+
"tag": false
|
|
359
|
+
},
|
|
360
|
+
"updateInternalDependencies": "patch",
|
|
361
|
+
"ignore": []
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`npx changeset init` writes a starting point — it prompts once, for whether to
|
|
366
|
+
use the GitHub changelog integration, and the answer here is no. Four keys then
|
|
367
|
+
need setting, and each is a decision rather than a preference.
|
|
368
|
+
|
|
369
|
+
- **`changelog: "@changesets/changelog-git"`** attributes each entry to its
|
|
370
|
+
commit. The GitHub changelog generator wants a token and produces entries
|
|
371
|
+
naming pull requests, which is history the changelog does not need.
|
|
372
|
+
- **`privatePackages: {version: true, tag: false}`** is what makes changesets
|
|
373
|
+
work at all here. Every package in the constellation is `private`, because it
|
|
374
|
+
ships as a Foundry release rather than to npm; without `version: true`
|
|
375
|
+
changesets skips it entirely, and `tag: false` because the release tag is cut
|
|
376
|
+
by the release workflow rather than by changesets.
|
|
377
|
+
- **`commit: true`** so `changeset version` commits its own result.
|
|
378
|
+
- **`baseBranch: "main"`** is what `changeset status --since=origin/main`
|
|
379
|
+
compares against.
|
|
380
|
+
|
|
381
|
+
Then one `.changeset/*.md` per unreleased change:
|
|
382
|
+
|
|
383
|
+
```markdown
|
|
384
|
+
---
|
|
385
|
+
"acme-bestiary": minor
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
**Creatures** — The marsh drake joins the bestiary, with its habits and the
|
|
389
|
+
stretch of water it holds.
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Three rules about the body, and they are enforced by review rather than by a
|
|
393
|
+
tool:
|
|
394
|
+
|
|
395
|
+
- **It describes the setting, not the work.** Write for someone who installs or
|
|
396
|
+
upgrades and wants to know what changed for them.
|
|
397
|
+
- **Bold labels, never `#` headings.** Changesets nests the whole summary inside
|
|
398
|
+
one bullet, so an `h2` written here becomes a real `<h2>` in the changelog,
|
|
399
|
+
outranking the `Minor Changes` heading above it and polluting the outline.
|
|
400
|
+
- **On a 0.x package a breaking change is `minor`, never `major`.** `major`
|
|
401
|
+
takes the package to 1.0.0, which is a product decision rather than a
|
|
402
|
+
description of a diff.
|
|
403
|
+
|
|
404
|
+
## `.github/`
|
|
405
|
+
|
|
406
|
+
Two files the toolchain reads, and they must agree.
|
|
407
|
+
|
|
408
|
+
**`.github/labels.yml`** is the closed label registry — the machine-readable
|
|
409
|
+
source of truth, synced to GitHub by the org-wide `labels` action. A label on
|
|
410
|
+
GitHub that is not listed there is deleted on the next sync, which is what makes
|
|
411
|
+
the set closed and why nothing invents one.
|
|
412
|
+
|
|
413
|
+
```yaml
|
|
414
|
+
- name: documentation
|
|
415
|
+
color: "0075ca"
|
|
416
|
+
description: Documentation about this repository — README, process, authoring guides.
|
|
417
|
+
|
|
418
|
+
- name: devops
|
|
419
|
+
color: "e07b31"
|
|
420
|
+
description: Build, tooling, CI, release, repo config.
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**`.github/ISSUE_REPORTING.md`** §3 is the same list in prose, for a human
|
|
424
|
+
filing an issue. `package-build labels check` compares the two and fails when
|
|
425
|
+
they disagree:
|
|
426
|
+
|
|
427
|
+
```console
|
|
428
|
+
$ npx package-build labels check
|
|
429
|
+
package-build: registry and §3 agree (2 labels).
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Neither file derives from the other, so this check is the only thing standing
|
|
433
|
+
between them and a silent divergence. Editing the registry means editing both.
|
|
434
|
+
|
|
435
|
+
Labels are **subject matter only**. Work shape — bug, feature, epic, task, spike
|
|
436
|
+
— is an issue _type_, shared across the organization, so there is no `bug` label
|
|
437
|
+
and filtering on one returns nothing.
|
|
438
|
+
|
|
439
|
+
Neither file exists in a fresh repository, and `labels check` says so rather
|
|
440
|
+
than passing vacuously:
|
|
441
|
+
|
|
442
|
+
```console
|
|
443
|
+
$ npx package-build labels check
|
|
444
|
+
package-build: labels check: .github/labels.yml does not exist.
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
`.github/workflows/` then carries the repository's own CI. The org-wide
|
|
448
|
+
`HeroicLands/.github` repository supplies the shared composite actions those
|
|
449
|
+
workflows `uses:` — `labels`, `no-attribution` and `todos` — plus the reusable
|
|
450
|
+
site-deploy workflow, so a fix to any of them reaches every repository at once.
|
|
451
|
+
|
|
452
|
+
## What the toolchain reads, and what it writes
|
|
453
|
+
|
|
454
|
+
A summary, because "where does this come from" is the question that recurs.
|
|
455
|
+
|
|
456
|
+
| Read | By |
|
|
457
|
+
| -------------------------------------------------- | ----------------------------------------------------------- |
|
|
458
|
+
| `package.json` | The Foundry package id, the version, the release addresses. |
|
|
459
|
+
| `package-build.config.yaml` | Everything else about the build. |
|
|
460
|
+
| `assets/content/**/*.md` | Every content command. |
|
|
461
|
+
| `.gitignore` | `content-build format`, `content-build markdown`. |
|
|
462
|
+
| `.prettierignore` | `content-build format`. |
|
|
463
|
+
| `.github/labels.yml`, `.github/ISSUE_REPORTING.md` | `package-build labels check`. |
|
|
464
|
+
| `lang/*.json` | `package-build lang`. |
|
|
465
|
+
| `.env.local` | `package-build deploy`. |
|
|
466
|
+
| `.github/workflows/build.yml` | The `pre-push` hook, for its step list. |
|
|
467
|
+
|
|
468
|
+
Everything written goes under `build/`. Nothing the toolchain generates is
|
|
469
|
+
committed.
|
|
@@ -576,33 +576,6 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
576
576
|
return referencedSubtype(this.documentSubtypes, type, "Item");
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
-
/**
|
|
580
|
-
* Resolve one embedded item from a `(type, shortcode?, overlay)`
|
|
581
|
-
* descriptor. If `shortcode` is given, the predefined item is fetched
|
|
582
|
-
* from `itemsMap` and the overlay deep-merged on top. If absent, the
|
|
583
|
-
* descriptor must carry enough fields to stand alone. The embedded
|
|
584
|
-
* item's `_id` is regenerated deterministically from
|
|
585
|
-
* `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
|
|
586
|
-
* from the **document subtype**, so that renaming a note type leaves
|
|
587
|
-
* every embedded id exactly where it was.
|
|
588
|
-
* Returns null if the descriptor cannot be resolved.
|
|
589
|
-
*
|
|
590
|
-
* @param {Map<string, object>} itemsMap - The predefined items, by address.
|
|
591
|
-
* @param {string} actorId - The owning actor's id, seeding embedded ids.
|
|
592
|
-
* @param {string} type - The **note** type the reference names.
|
|
593
|
-
* @param {string|null} shortcode - The referenced item's shortcode, or
|
|
594
|
-
* `null` for a stand-alone entry.
|
|
595
|
-
* @param {object} [overlay] - The entry's remaining properties.
|
|
596
|
-
* @param {string} indexKey - Where the reference sits, for a diagnostic.
|
|
597
|
-
* It no longer reaches the id — it names the entry in a message.
|
|
598
|
-
* @param {string} ctx - Diagnostic context (the actor's label).
|
|
599
|
-
* @param {object} [at] - Where to locate a finding.
|
|
600
|
-
* @param {string} [at.fmKey] - The frontmatter key the reference sits
|
|
601
|
-
* under, so an unresolved one is reported at the reference rather than
|
|
602
|
-
* at the note.
|
|
603
|
-
* @returns {object|null} The embedded item, or null when it resolved to
|
|
604
|
-
* nothing — always with a finding emitted.
|
|
605
|
-
*/
|
|
606
579
|
/**
|
|
607
580
|
* Read an entry's `model:` — the address of the item it is a copy of.
|
|
608
581
|
*
|
|
@@ -653,6 +626,36 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
653
626
|
return { type: read.type, shortcode: read.shortcode, package: read.package ?? null };
|
|
654
627
|
}
|
|
655
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Resolve one embedded item from a `(type, shortcode?, overlay)`
|
|
631
|
+
* descriptor. If `shortcode` is given, the predefined item is fetched
|
|
632
|
+
* from `itemsMap` and the overlay deep-merged on top. If absent, the
|
|
633
|
+
* descriptor must carry enough fields to stand alone. The embedded
|
|
634
|
+
* item's `_id` is regenerated deterministically from
|
|
635
|
+
* `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
|
|
636
|
+
* from the **document subtype**, so that renaming a note type leaves
|
|
637
|
+
* every embedded id exactly where it was.
|
|
638
|
+
* Returns null if the descriptor cannot be resolved.
|
|
639
|
+
*
|
|
640
|
+
* @param {Map<string, object>} itemsMap - The predefined items, by address.
|
|
641
|
+
* @param {string} actorId - The owning actor's id, seeding embedded ids.
|
|
642
|
+
* @param {string} type - The **note** type the reference names.
|
|
643
|
+
* @param {string|null} shortcode - The referenced item's shortcode, or
|
|
644
|
+
* `null` for a stand-alone entry.
|
|
645
|
+
* @param {object} overlay - The entry's remaining properties.
|
|
646
|
+
* @param {string} indexKey - Where the reference sits, for a diagnostic.
|
|
647
|
+
* It names the entry in a message and does not reach the id.
|
|
648
|
+
* @param {string} ctx - Diagnostic context (the actor's label).
|
|
649
|
+
* @param {object} [at] - Where to locate a finding.
|
|
650
|
+
* @param {string} [at.fmKey] - The frontmatter key the reference sits
|
|
651
|
+
* under, so an unresolved one is reported at the reference rather than
|
|
652
|
+
* at the note.
|
|
653
|
+
* @param {string|null} [at.modelPackage] - The package a `model:` names,
|
|
654
|
+
* where the template comes from another package's catalogue. Given one,
|
|
655
|
+
* the packaged address is what resolves and nothing local shadows it.
|
|
656
|
+
* @returns {object|null} The embedded item, or null when it resolved to
|
|
657
|
+
* nothing — always with a finding emitted.
|
|
658
|
+
*/
|
|
656
659
|
resolveEmbedded(
|
|
657
660
|
itemsMap,
|
|
658
661
|
actorId,
|
package/engine/address-diff.mjs
CHANGED
|
@@ -173,47 +173,6 @@ export function readItemAddresses(dirs) {
|
|
|
173
173
|
return space;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
/**
|
|
177
|
-
* Every rename the tree's notes **declare**, as old address → where it went.
|
|
178
|
-
*
|
|
179
|
-
* Read from the content tree rather than from compiled output, because a
|
|
180
|
-
* declaration is authored and the compiled document does not carry it: nothing
|
|
181
|
-
* downstream consumes `renamedFrom:`, so emitting it into every pack to let one
|
|
182
|
-
* diagnostic read it back would put a build-time note in shipped data forever.
|
|
183
|
-
* The tree is already read by this module for the same reason
|
|
184
|
-
* ({@link noteFilesById}) — to place a finding where its author can fix it.
|
|
185
|
-
*
|
|
186
|
-
* **A declaration is keyed by document subtype, not by note type.** The address
|
|
187
|
-
* space is the one consumers resolve against, and it is spelled in compiled
|
|
188
|
-
* documents: `hm3` compiles a `projectile` note into a `missilegear` item,
|
|
189
|
-
* so that is the address a rename of it moves. {@link referencedSubtype} is the
|
|
190
|
-
* function that already answers this for a being's embedded `(type, shortcode)`
|
|
191
|
-
* references, so both sides read the same rule rather than a second copy of it.
|
|
192
|
-
*
|
|
193
|
-
* **An entry is emitted for every system that maps the type**, whether or not
|
|
194
|
-
* the note declares that system's block. Over-emitting is inert — the diff uses
|
|
195
|
-
* an entry only when the baseline published the old address *and* this build
|
|
196
|
-
* publishes the new one, and a system the note does not compile for satisfies
|
|
197
|
-
* neither — while asking which blocks a note declares would put a second,
|
|
198
|
-
* subtly different answer to that question in a third place.
|
|
199
|
-
*
|
|
200
|
-
* **First claim wins on a collision.** Two notes naming one predecessor is a
|
|
201
|
-
* contradiction — an address has one successor — and it is reported as an error
|
|
202
|
-
* by `engine/content-lint.mjs`, where both notes are in hand and can both be
|
|
203
|
-
* named. Picking one here keeps this a map; it is not a resolution, and nothing
|
|
204
|
-
* rests on which one it picked.
|
|
205
|
-
*
|
|
206
|
-
* @param {string} contentBase - Root of the content tree.
|
|
207
|
-
* @param {object} opts
|
|
208
|
-
* @param {readonly string[]} [opts.skipDirectories] - The corpus scope. Stated
|
|
209
|
-
* by the caller, never defaulted — see {@link addressCorpus}.
|
|
210
|
-
* @param {readonly object[]} [opts.maps] - The document-subtype maps.
|
|
211
|
-
* @param {object} [opts.config] - The resolved build configuration.
|
|
212
|
-
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
213
|
-
* derived, shared with {@link noteFilesById} so one command reads one corpus.
|
|
214
|
-
* @returns {Map<string, {to: string, file: string, shortcode: string}>} Old
|
|
215
|
-
* address → the address the declaring note publishes at now, and that note.
|
|
216
|
-
*/
|
|
217
176
|
/**
|
|
218
177
|
* The corpus both reads below share, as content-index records.
|
|
219
178
|
*
|
|
@@ -258,6 +217,49 @@ function addressCorpus(contentBase, { skipDirectories, config, records, problems
|
|
|
258
217
|
return indexRecordsFor({ contentBase, config, skipDirectories, problems });
|
|
259
218
|
}
|
|
260
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Every rename the tree's notes **declare**, as old address → where it went.
|
|
222
|
+
*
|
|
223
|
+
* Read from the content tree rather than from compiled output, because a
|
|
224
|
+
* declaration is authored and the compiled document does not carry it: nothing
|
|
225
|
+
* downstream consumes `renamedFrom:`, so emitting it into every pack to let one
|
|
226
|
+
* diagnostic read it back would put a build-time note in shipped data forever.
|
|
227
|
+
* The tree is already read by this module for the same reason
|
|
228
|
+
* ({@link noteFilesById}) — to place a finding where its author can fix it.
|
|
229
|
+
*
|
|
230
|
+
* **A declaration is keyed by document subtype, not by note type.** The address
|
|
231
|
+
* space is the one consumers resolve against, and it is spelled in compiled
|
|
232
|
+
* documents: `hm3` compiles a `projectile` note into a `missilegear` item,
|
|
233
|
+
* so that is the address a rename of it moves. {@link referencedSubtype} is the
|
|
234
|
+
* function that already answers this for a being's embedded `(type, shortcode)`
|
|
235
|
+
* references, so both sides read the same rule rather than a second copy of it.
|
|
236
|
+
*
|
|
237
|
+
* **An entry is emitted for every system that maps the type**, whether or not
|
|
238
|
+
* the note declares that system's block. Over-emitting is inert — the diff uses
|
|
239
|
+
* an entry only when the baseline published the old address *and* this build
|
|
240
|
+
* publishes the new one, and a system the note does not compile for satisfies
|
|
241
|
+
* neither — while asking which blocks a note declares would put a second,
|
|
242
|
+
* subtly different answer to that question in a third place.
|
|
243
|
+
*
|
|
244
|
+
* **First claim wins on a collision.** Two notes naming one predecessor is a
|
|
245
|
+
* contradiction — an address has one successor — and it is reported as an error
|
|
246
|
+
* by `engine/content-lint.mjs`, where both notes are in hand and can both be
|
|
247
|
+
* named. Picking one here keeps this a map; it is not a resolution, and nothing
|
|
248
|
+
* rests on which one it picked.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} contentBase - Root of the content tree.
|
|
251
|
+
* @param {object} opts
|
|
252
|
+
* @param {readonly string[]} [opts.skipDirectories] - The corpus scope. Stated
|
|
253
|
+
* by the caller, never defaulted — see {@link addressCorpus}.
|
|
254
|
+
* @param {readonly object[]} [opts.maps] - The document-subtype maps.
|
|
255
|
+
* @param {object} [opts.config] - The resolved build configuration.
|
|
256
|
+
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
257
|
+
* derived, shared with {@link noteFilesById} so one command reads one corpus.
|
|
258
|
+
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
259
|
+
* record, so one of them does not abort the diff before it reports.
|
|
260
|
+
* @returns {Map<string, {to: string, file: string, shortcode: string}>} Old
|
|
261
|
+
* address → the address the declaring note publishes at now, and that note.
|
|
262
|
+
*/
|
|
261
263
|
export function declaredPredecessors(
|
|
262
264
|
contentBase,
|
|
263
265
|
{ skipDirectories, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS, config, records, problems } = {},
|
|
@@ -394,6 +396,8 @@ export function diffItemAddresses(baseline, current, { baseline: label, predeces
|
|
|
394
396
|
* id is derived against. See {@link addressCorpus} for why that matters.
|
|
395
397
|
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
396
398
|
* derived, shared with {@link declaredPredecessors}.
|
|
399
|
+
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
400
|
+
* record, so one of them does not abort the diff before it reports.
|
|
397
401
|
* @returns {Map<string, string>} Document id → the note's absolute path.
|
|
398
402
|
*/
|
|
399
403
|
export function noteFilesById(contentBase, { skipDirectories, config, records, problems } = {}) {
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -303,11 +303,17 @@ export class BasePackCompiler {
|
|
|
303
303
|
* Resolves a `packFolder` — a folder note's address — to the Foundry
|
|
304
304
|
* folder id it materialises as in this pack.
|
|
305
305
|
* @param {string} [options.packName] - The pack this pass writes.
|
|
306
|
+
* @param {string|null} [options.packSystem] - The system that pack declares,
|
|
307
|
+
* which decides the `_stats` it stamps and the system block a note must
|
|
308
|
+
* carry to reach it. `null` for a pack that declares none.
|
|
306
309
|
* @param {string} [options.docType] - The Foundry document type it holds.
|
|
307
310
|
* @param {{resolve: Function}} [options.router] - The pack router. Omit it
|
|
308
311
|
* — as the unit suite does — and every claimed note is compiled here.
|
|
309
312
|
* @param {boolean} [options.routingReporter] - Whether this pass reports a
|
|
310
313
|
* note of its type that routes nowhere.
|
|
314
|
+
* @param {object} [options.corpus] - The corpus this compile is running
|
|
315
|
+
* over, derived once and shared by every pass. A pass handed none derives
|
|
316
|
+
* its own in `prepare`, and reports that corpus's problems itself.
|
|
311
317
|
*/
|
|
312
318
|
constructor({
|
|
313
319
|
contentBase,
|