@heroiclands/package-build 0.2.0 → 0.3.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 +187 -0
- package/README.md +104 -0
- package/bin/package-build.mjs +68 -0
- package/config.mjs +76 -0
- package/manifest.mjs +152 -96
- package/package.json +7 -3
- package/types/config.d.mts +28 -0
- package/types/manifest.d.mts +48 -52
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @heroiclands/package-build
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0a2ef1e: **The Foundry manifest is generated from configuration. The template is
|
|
8
|
+
retired.**
|
|
9
|
+
|
|
10
|
+
`package-build manifest` writes `system.json` / `module.json` from
|
|
11
|
+
`packageBuild.manifest` plus the facts the build already holds. There is no
|
|
12
|
+
`assets/templates/*.template.json`, and the template-reading path is removed
|
|
13
|
+
rather than left as a fallback: `writeFoundryManifest`, `stampManifest` and
|
|
14
|
+
`artifactFromTemplate` are gone, replaced by `buildManifest`, `writeManifest`
|
|
15
|
+
and `manifestPacks`.
|
|
16
|
+
|
|
17
|
+
The manifest was the one build input still hand-authored JSON, per repository,
|
|
18
|
+
with no schema and nothing checking it — and it declared facts the configuration
|
|
19
|
+
already declared. SoHL's pack list was written twice, in two formats, with
|
|
20
|
+
nothing checking the pairs agreed; `sohl-kethira-basic` hand-maintained its whole
|
|
21
|
+
`module.json`, and its `download` named an older version than the module claimed.
|
|
22
|
+
|
|
23
|
+
Three kinds of key end up in the result:
|
|
24
|
+
|
|
25
|
+
- **Declared** — `packageBuild.manifest`, emitted unchanged, so a key Foundry
|
|
26
|
+
adds in a later version needs no release of this package. The block is
|
|
27
|
+
deliberately not key-checked; pass-through and unknown-key checking cannot
|
|
28
|
+
coexist, which is why it is its own block rather than spread across
|
|
29
|
+
`packageBuild:` where the keys around it are still checked.
|
|
30
|
+
- **Derived** — `id`, `version`, `url`, `bugs`, `manifest`, `download`,
|
|
31
|
+
`compatibility`, `relationships`, `packs`. Declaring one is an **error**
|
|
32
|
+
naming the key and where the value actually comes from, not an override: an
|
|
33
|
+
authored copy would be silently overwritten and the two would disagree with
|
|
34
|
+
nothing to say so.
|
|
35
|
+
- **Computed** — namespaced `flags` from a module named in
|
|
36
|
+
`packageBuild.manifestFlags`, merged over any declared. That is for a value a
|
|
37
|
+
repository must work out rather than state — SoHL's credits `@UUID` only
|
|
38
|
+
exists once the content tree has been walked.
|
|
39
|
+
|
|
40
|
+
`packs` comes from the **one** pack list at the top level of
|
|
41
|
+
`content-build.config.yaml`, with companions flattened in. Give each pack the
|
|
42
|
+
`label` Foundry should show; everything else is derived.
|
|
43
|
+
|
|
44
|
+
Requires `@heroiclands/content-build` **1.0.0**, which moved `compatibility` and
|
|
45
|
+
`relationships` to the top level (content-build#50).
|
|
46
|
+
|
|
47
|
+
Verified against SoHL's real package: the generated manifest is **byte-identical**
|
|
48
|
+
to what its template pipeline produces today, all 24 keys, key order included.
|
|
49
|
+
|
|
50
|
+
## 0.2.1
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- 4144291: **Release from merged changesets instead of a remembered command**
|
|
55
|
+
|
|
56
|
+
Fixes [#4](https://github.com/HeroicLands/package-build/issues/4). Releasing was
|
|
57
|
+
hand-driven — bump `package.json` on a branch, merge, then remember
|
|
58
|
+
`gh release create`, because cutting the Release is what published. Nothing
|
|
59
|
+
enforced the last step, so a merged version could sit unpublished with no check
|
|
60
|
+
red; the sibling repository lost two versions that way.
|
|
61
|
+
|
|
62
|
+
- Every pull request now declares its bump as a `.changeset/*.md` file, and CI's
|
|
63
|
+
**Changeset declared** job fails one that does not. `npx changeset add --empty`
|
|
64
|
+
is how a change says it needs no release — explicitly, rather than by omission.
|
|
65
|
+
- Merging to `main` opens a **Version Packages** pull request carrying the bump
|
|
66
|
+
and the rewritten `CHANGELOG.md`. An unreleased state is now a pull request
|
|
67
|
+
waiting in the queue rather than nothing at all.
|
|
68
|
+
- Merging that runs `changeset publish`: npm publish, the `v<version>` tag, and
|
|
69
|
+
the GitHub Release with the changelog section as its body. The OIDC Trusted
|
|
70
|
+
Publishing step is unchanged and still last; there is still no `NPM_TOKEN`, and
|
|
71
|
+
re-running on a published version is a no-op.
|
|
72
|
+
- `CHANGELOG.md` is seeded from the two hand-cut Releases so far and now ships
|
|
73
|
+
with the package. A changeset is also where a raised dependency floor gets
|
|
74
|
+
recorded — 0.2.0 raised one to `@heroiclands/content-build >= 0.15.0` and said
|
|
75
|
+
so nowhere.
|
|
76
|
+
|
|
77
|
+
<!-- Sections at 0.2.1 and above are generated by `changeset version` from the
|
|
78
|
+
changesets merged into `main`. Sections at 0.2.0 and below predate that
|
|
79
|
+
pipeline and are the hand-written GitHub Release notes, kept verbatim
|
|
80
|
+
(headings demoted one level to sit under their version) so no history was
|
|
81
|
+
lost in adopting it. -->
|
|
82
|
+
|
|
83
|
+
## 0.2.0
|
|
84
|
+
|
|
85
|
+
_2026-08-22 — a command line, not a wrapper script per job_
|
|
86
|
+
|
|
87
|
+
**A command line, so a consumer writes configuration instead of scripts.**
|
|
88
|
+
|
|
89
|
+
This package was library-only, so every consuming repository wrote a wrapper script per job — six of them in the Song of Heroic Lands repository, 441 lines that between them contained no logic:
|
|
90
|
+
|
|
91
|
+
| Wrapper | Lines | What was in it |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `build-system-json.mjs` | 143 | |
|
|
94
|
+
| `push-stage.mjs` | 76 | argv, dotenv, then `packageKind: "systems"` and `packageId: "sohl"` hard-coded beside a configuration that already declared both |
|
|
95
|
+
| `copy-assets.mjs` | 71 | a nine-entry data table, plus one genuine repository-specific transform |
|
|
96
|
+
| `check-lang.mjs` | 69 | a glob, a call, and a help string |
|
|
97
|
+
| `clean.mjs` | 47 | a `repoRoot` from `import.meta.url`, one flag, one call — no consumer-specific value at all |
|
|
98
|
+
| `pack-release.mjs` | 35 | one call passing `{ artifact: "system" }`, which `packageKind` already decides |
|
|
99
|
+
|
|
100
|
+
Every copy had drifted from its sibling in the other repositories, because copies do: `clean.mjs` was 47 lines in SoHL and 48 in `sohl-thalorna`, `copy-assets.mjs` 71 and 76. `sohl-thalorna`'s copy still reimplements the recursive directory copy this package has exported since it was extracted, because it was written before the extraction and nobody went back.
|
|
101
|
+
|
|
102
|
+
It is the same shape the configuration had before it became data: not logic, but the boilerplate a code file needs in order to *state a literal*. So the literals moved into configuration, and the boilerplate lives here, once.
|
|
103
|
+
|
|
104
|
+
#### The commands
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
npx package-build clean [--distclean]
|
|
108
|
+
npx package-build assets
|
|
109
|
+
npx package-build lang check
|
|
110
|
+
npx package-build release
|
|
111
|
+
npx package-build deploy <stage>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Wrapped as npm scripts:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"clean": "package-build clean",
|
|
119
|
+
"build:assets": "package-build assets",
|
|
120
|
+
"lint:lang": "package-build lang check",
|
|
121
|
+
"build:pack-release": "package-build release",
|
|
122
|
+
"push:qa": "package-build deploy qa"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### One configuration file, not two
|
|
127
|
+
|
|
128
|
+
Settings come from the reserved `packageBuild:` section of `content-build.config.yaml` — the file a repository already has — which requires **`@heroiclands/content-build` 0.15.0 or later**.
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
packageKind: systems # read from the top level, never restated below
|
|
132
|
+
foundryPackage: sohl
|
|
133
|
+
|
|
134
|
+
packageBuild:
|
|
135
|
+
assets:
|
|
136
|
+
- { from: lang, to: lang }
|
|
137
|
+
assetTransform: ./utils/svg-theme.mjs
|
|
138
|
+
clean:
|
|
139
|
+
extra: [site/content, site/public]
|
|
140
|
+
deploy:
|
|
141
|
+
envPrefix: SOHL
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A second config file would have restated `packageKind` and `foundryPackage`, which is two places for one fact. content-build validates only that the section is a mapping and hands it back frozen; everything inside it is validated here, so neither package learns the other's schema.
|
|
145
|
+
|
|
146
|
+
**Derived, never stated:** the repository root (the config file's own location), `packageKind` and `packageId` (the shared configuration's top level), and the release artifact — a system ships `system.json`, a module `module.json`, so the kind already decides it.
|
|
147
|
+
|
|
148
|
+
**The one genuine piece of consumer code stays the consumer's.** SoHL rewrites each SVG's hard-coded fill so icons follow the Foundry theme; `packageBuild.assetTransform` names a module exporting `transform(sourcePath) -> string | null`.
|
|
149
|
+
|
|
150
|
+
#### Also in this release
|
|
151
|
+
|
|
152
|
+
- `exports["./config"]` now ships its type declaration. It was declared but never generated — `tsconfig.dts.json` lists its inputs explicitly and `config.mjs` was missing from the list — so the subpath would have pointed at a file that does not exist.
|
|
153
|
+
- The library modules are unchanged. Every signature the CLI needed was already there.
|
|
154
|
+
|
|
155
|
+
#### Notes
|
|
156
|
+
|
|
157
|
+
`--version` and `--help` answer in a directory with no configuration at all. Failures report one line rather than a stack, whether the handler was synchronous or asynchronous. Localization findings follow the diagnostics contract, `file:line:column: severity: message`.
|
|
158
|
+
|
|
159
|
+
**Full changelog:** https://github.com/HeroicLands/package-build/compare/v0.1.0...v0.2.0
|
|
160
|
+
|
|
161
|
+
## 0.1.0
|
|
162
|
+
|
|
163
|
+
_2026-08-22 — the Foundry package toolchain_
|
|
164
|
+
|
|
165
|
+
The counterpart to [`@heroiclands/content-build`](https://github.com/HeroicLands/content-build). The two split by **input**, not by repository: content-build reads `assets/content/**` and answers for what a package *says*; this one reads `lang/`, `styles/`, `src/`, `assets/` and the manifest template, and answers for what a package *is* — the parts Foundry loads whether or not the package ships any content.
|
|
166
|
+
|
|
167
|
+
A module uses either, or both. An adventure module that ships only notes needs no bundler; a variant module that ships only behavior needs no Markdown pipeline. The coupling runs one way: package-build asks content-build for the compiled `packs[]` block, never the reverse.
|
|
168
|
+
|
|
169
|
+
### Seven modules — the whole of assemble → validate → ship
|
|
170
|
+
|
|
171
|
+
- **`manifest`** — `system.json` / `module.json`. The artifact is inferred from the template's name, and every address is derived from `package.json`'s `repository` rather than transcribed. Handles the `git+https://….git` spelling npm writes, which yields a 404 on every Foundry update check if left in place.
|
|
172
|
+
- **`stage`** — assembling the build stage and clearing it away. A listed asset that does not exist **fails the build** instead of shipping a package that quietly lacks its localization or templates; the whole list is checked before anything is copied.
|
|
173
|
+
- **`lang`** — what a shippable localization file must satisfy: it parses, its top level is an object, no key is both a leaf and a dotted prefix of another (which makes Foundry discard the entire file), placeholders are single-braced, key segments carry no data.
|
|
174
|
+
- **`bundle`** — whether the manifest agrees with the file it points at. Declared under `"esmodules"` the bundle must parse as a module; declared under `"scripts"` it must declare **nothing** at top level, because each top-level declaration in a classic script is a global lexical binding and one colliding with a non-configurable `window` property throws at parse time. That rule bricked SoHL v0.8.0.
|
|
175
|
+
- **`release`** — the two assets a GitHub Release carries. Waits for the archive to be *written*, not merely finalized.
|
|
176
|
+
- **`deploy`** — installing into a Foundry data directory, local or SFTP, always as a staged atomic swap: a running Foundry holds its LevelDB packs open, and replacing them in place leaves a directory LevelDB "repairs" to zero.
|
|
177
|
+
- **`text`** — locating a literal, so a finding names its line and column.
|
|
178
|
+
|
|
179
|
+
### Design
|
|
180
|
+
|
|
181
|
+
The rules are pure functions over data; the functions that touch disk or a network are named for what they do. That is what makes them testable at all — the scripts they were extracted from each ran their work at import time and exported nothing.
|
|
182
|
+
|
|
183
|
+
### Provenance
|
|
184
|
+
|
|
185
|
+
Developed as a workspace inside the Song of Heroic Lands repository across six changes (SoHL#1680, #1682, #1684, #1685, #1686), exactly as content-build incubated before its own extraction, and extracted now that its shape has stopped changing.
|
|
186
|
+
|
|
187
|
+
128 tests; declaration files are emitted from the JSDoc at pack time.
|
package/README.md
CHANGED
|
@@ -95,8 +95,74 @@ packageBuild:
|
|
|
95
95
|
deploy:
|
|
96
96
|
# Prefix of the shared SFTP override variables. Default `SOHL`.
|
|
97
97
|
envPrefix: SOHL
|
|
98
|
+
|
|
99
|
+
# Optional. A module exporting `flags(config)` returning namespaced Foundry
|
|
100
|
+
# flags the repository has to *compute* — an address that only exists once
|
|
101
|
+
# the content tree has been walked, say. Merged over any declared below.
|
|
102
|
+
manifestFlags: ./utils/manifest-flags.mjs
|
|
103
|
+
|
|
104
|
+
# The Foundry package manifest. Emitted as declared, so a key Foundry adds in
|
|
105
|
+
# a later version needs no release of this package.
|
|
106
|
+
manifest:
|
|
107
|
+
title: Song of Heroic Lands
|
|
108
|
+
description: <p>…</p>
|
|
109
|
+
license: LICENSE.md
|
|
110
|
+
readme: README.md
|
|
111
|
+
authors:
|
|
112
|
+
- { name: Toasty, discord: "toasty#8538" }
|
|
113
|
+
esmodules: [sohl.js]
|
|
114
|
+
styles: [css/sohl.css]
|
|
115
|
+
languages:
|
|
116
|
+
- { lang: en, name: English, path: lang/en.json }
|
|
117
|
+
documentTypes:
|
|
118
|
+
Item:
|
|
119
|
+
skill: { htmlFields: [notes, docHtml] }
|
|
120
|
+
packFolders:
|
|
121
|
+
- name: Song of Heroic Lands
|
|
122
|
+
sorting: m
|
|
123
|
+
color: "#094fcb"
|
|
124
|
+
packs: [items, journals, actors, macros, scenes, adventures]
|
|
125
|
+
media:
|
|
126
|
+
- { type: logo, url: systems/sohl/assets/ui/logo.webp }
|
|
127
|
+
socket: true
|
|
128
|
+
grid: { distance: 5, units: ft }
|
|
129
|
+
primaryTokenAttribute: health
|
|
98
130
|
```
|
|
99
131
|
|
|
132
|
+
### The manifest is generated, not stamped
|
|
133
|
+
|
|
134
|
+
`package-build manifest` writes `system.json` / `module.json` into the stage.
|
|
135
|
+
**There is no template file.** A manifest used to be hand-authored JSON that the
|
|
136
|
+
build stamped a few fields into — the one build input still written by hand, per
|
|
137
|
+
repository, with no schema and nothing checking it. It also declared facts the
|
|
138
|
+
configuration already declared: the pack list twice, in two formats, with
|
|
139
|
+
nothing checking the pairs agreed.
|
|
140
|
+
|
|
141
|
+
Three kinds of key end up in the result:
|
|
142
|
+
|
|
143
|
+
| Kind | Where it comes from |
|
|
144
|
+
| ------------ | ------------------------------------------------------------------------------------------------- |
|
|
145
|
+
| **Declared** | `packageBuild.manifest`, emitted unchanged |
|
|
146
|
+
| **Derived** | `id`, `version`, `url`, `bugs`, `manifest`, `download`, `compatibility`, `relationships`, `packs` |
|
|
147
|
+
| **Computed** | namespaced `flags` from `manifestFlags`, merged over any declared |
|
|
148
|
+
|
|
149
|
+
**Declaring a derived key is an error, not an override.** An authored `version`
|
|
150
|
+
would look authoritative, sit there unread, and disagree with the shipped
|
|
151
|
+
package forever; the build says so, naming the key and where the value actually
|
|
152
|
+
comes from.
|
|
153
|
+
|
|
154
|
+
`packs` is derived from the **one** pack list at the top level of
|
|
155
|
+
`content-build.config.yaml` — each entry's `label`, `type`, `name` and
|
|
156
|
+
`private`, plus a `system` from `stats.systemId` and a `path` of
|
|
157
|
+
`packs/<name>`. Companions are flattened in, because Foundry sees no difference:
|
|
158
|
+
a companion is only a pack written by another pass rather than one of its own.
|
|
159
|
+
Give each pack the `label` you want Foundry to show.
|
|
160
|
+
|
|
161
|
+
`compatibility` and `relationships` are read from the **top level** of the
|
|
162
|
+
shared configuration, not from this section — content-build consumes them
|
|
163
|
+
(`supportedCoreVersion`, and a module'''s `stats.systemVersion`) and the
|
|
164
|
+
dependency runs one way.
|
|
165
|
+
|
|
100
166
|
**Why one file and not two.** Two of the values this package needs —
|
|
101
167
|
`packageKind` and `foundryPackage` — are already declared for `content-build`. A
|
|
102
168
|
second config file would restate them, which is two places for one fact; that is
|
|
@@ -121,6 +187,7 @@ other's schema — they split by input, and the dependency runs one way.
|
|
|
121
187
|
```
|
|
122
188
|
npx package-build clean [--distclean]
|
|
123
189
|
npx package-build assets
|
|
190
|
+
npx package-build manifest
|
|
124
191
|
npx package-build lang check
|
|
125
192
|
npx package-build release
|
|
126
193
|
npx package-build deploy <stage>
|
|
@@ -198,6 +265,43 @@ would let something reach for one.
|
|
|
198
265
|
declared `dependency`. Inside a workspace a missing declaration is invisible;
|
|
199
266
|
installed from npm it fails on the first import.
|
|
200
267
|
|
|
268
|
+
## Releasing
|
|
269
|
+
|
|
270
|
+
Releasing is not a command anyone runs. It is a consequence of merging, in two
|
|
271
|
+
steps, and each step is visible while it is pending.
|
|
272
|
+
|
|
273
|
+
**Every pull request declares its bump.** Run `npx changeset` and pick
|
|
274
|
+
major/minor/patch; the summary you write becomes the changelog entry and the
|
|
275
|
+
release note. If the change ships nothing a consumer can see, say so explicitly
|
|
276
|
+
with `npx changeset add --empty`. CI's **Changeset declared** job fails a pull
|
|
277
|
+
request that declares neither — `npm run changeset:check` is the same check,
|
|
278
|
+
locally.
|
|
279
|
+
|
|
280
|
+
**Merging to `main` opens a Version Packages pull request** carrying the version
|
|
281
|
+
bump and the rewritten `CHANGELOG.md`. That pull request _is_ the pending
|
|
282
|
+
release: as long as something is merged but unpublished, there is an open pull
|
|
283
|
+
request saying so. The previous, hand-driven process failed by leaving _nothing_
|
|
284
|
+
behind when its final step was forgotten (#4).
|
|
285
|
+
|
|
286
|
+
**Merging that publishes.** `changeset publish` puts the version on npm through
|
|
287
|
+
Trusted Publishing (OIDC — there is no `NPM_TOKEN`), tags the commit `v<version>`
|
|
288
|
+
and cuts the GitHub Release with the changelog section as its body. It publishes
|
|
289
|
+
only versions that are not already on the registry, so re-running it is a no-op;
|
|
290
|
+
`workflow_dispatch` on **Publish to npm** is the recovery path if a run fails
|
|
291
|
+
after versioning.
|
|
292
|
+
|
|
293
|
+
A changeset is also where a **raised dependency floor** gets recorded. 0.2.0 began
|
|
294
|
+
requiring `@heroiclands/content-build >= 0.15.0` and announced it nowhere; a
|
|
295
|
+
changeset is the place that now happens.
|
|
296
|
+
|
|
297
|
+
Below 1.0.0, `^0.x` never crosses a minor — a consumer on `^0.2.0` will not see
|
|
298
|
+
`0.3.0` until it bumps the pin deliberately, and Dependabot raises that as its own
|
|
299
|
+
pull request.
|
|
300
|
+
|
|
301
|
+
> After a successful publish, `npm view @heroiclands/package-build version` can
|
|
302
|
+
> report the _previous_ version for a minute or so. `dist-tags` is correct
|
|
303
|
+
> immediately, and is what the workflow prints.
|
|
304
|
+
|
|
201
305
|
## Licence
|
|
202
306
|
|
|
203
307
|
GPL-3.0-or-later.
|
package/bin/package-build.mjs
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* Usage:
|
|
43
43
|
* npx package-build clean [--distclean]
|
|
44
44
|
* npx package-build assets
|
|
45
|
+
* npx package-build manifest
|
|
45
46
|
* npx package-build lang check
|
|
46
47
|
* npx package-build release
|
|
47
48
|
* npx package-build deploy <stage>
|
|
@@ -62,9 +63,11 @@ import yargs from "yargs";
|
|
|
62
63
|
import { hideBin } from "yargs/helpers";
|
|
63
64
|
|
|
64
65
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
66
|
+
import { loadPackConfig } from "@heroiclands/content-build/engine/pack-config";
|
|
65
67
|
import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
|
|
66
68
|
import { validateLangSource } from "../lang.mjs";
|
|
67
69
|
import { packRelease } from "../release.mjs";
|
|
70
|
+
import { writeManifest } from "../manifest.mjs";
|
|
68
71
|
import { deployStage } from "../deploy.mjs";
|
|
69
72
|
|
|
70
73
|
/**
|
|
@@ -212,6 +215,70 @@ function assetsCommand() {
|
|
|
212
215
|
};
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
/**
|
|
219
|
+
* `manifest` — generate `system.json` / `module.json` into the build stage.
|
|
220
|
+
*
|
|
221
|
+
* There is no template to read. Everything the manifest needs is either
|
|
222
|
+
* declared in `packageBuild.manifest`, derived from configuration this
|
|
223
|
+
* repository already carries, or computed by a module the repository names in
|
|
224
|
+
* `packageBuild.manifestFlags` — for a namespaced flag it has to work out, such
|
|
225
|
+
* as the compendium address of a document that only exists once the content
|
|
226
|
+
* tree has been walked.
|
|
227
|
+
*
|
|
228
|
+
* @returns {object} The yargs command module.
|
|
229
|
+
*/
|
|
230
|
+
function manifestCommand() {
|
|
231
|
+
return {
|
|
232
|
+
command: "manifest",
|
|
233
|
+
describe: "Generate the Foundry package manifest",
|
|
234
|
+
builder: (y) => y,
|
|
235
|
+
handler: handler(async () => {
|
|
236
|
+
const config = loadPackageBuildConfig();
|
|
237
|
+
const shared = loadPackConfig();
|
|
238
|
+
const packageJson = JSON.parse(
|
|
239
|
+
fs.readFileSync(
|
|
240
|
+
path.join(config.rootDir, "package.json"),
|
|
241
|
+
"utf8",
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
let flags;
|
|
246
|
+
if (config.manifestFlags) {
|
|
247
|
+
const module = await import(
|
|
248
|
+
`file://${config.manifestFlags}`
|
|
249
|
+
).catch((err) =>
|
|
250
|
+
die(
|
|
251
|
+
`cannot load \`packageBuild.manifestFlags\` ` +
|
|
252
|
+
`(${config.manifestFlags}): ${err.message}`,
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
if (typeof module.flags !== "function") {
|
|
256
|
+
die(
|
|
257
|
+
`\`packageBuild.manifestFlags\` ` +
|
|
258
|
+
`(${config.manifestFlags}) exports no \`flags\` ` +
|
|
259
|
+
`function. It must export ` +
|
|
260
|
+
`\`flags(config) -> Record<string, object>\`.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
flags = await module.flags(shared);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const { path: written, manifest } = await writeManifest({
|
|
267
|
+
config: shared,
|
|
268
|
+
packageJson,
|
|
269
|
+
artifact: config.artifact,
|
|
270
|
+
outDir: path.join(config.rootDir, config.stageDir),
|
|
271
|
+
flags,
|
|
272
|
+
});
|
|
273
|
+
console.log(
|
|
274
|
+
`✅ Wrote ${path.relative(config.rootDir, written)} ` +
|
|
275
|
+
`(${Object.keys(manifest).length} keys, ` +
|
|
276
|
+
`${manifest.packs.length} packs).`,
|
|
277
|
+
);
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
215
282
|
/**
|
|
216
283
|
* `lang check` — verify every localization file survives `expandObject`.
|
|
217
284
|
*
|
|
@@ -352,6 +419,7 @@ yargs(hideBin(process.argv))
|
|
|
352
419
|
.scriptName("package-build")
|
|
353
420
|
.command(cleanCommand())
|
|
354
421
|
.command(assetsCommand())
|
|
422
|
+
.command(manifestCommand())
|
|
355
423
|
.command(langCommand())
|
|
356
424
|
.command(releaseCommand())
|
|
357
425
|
.command(deployCommand())
|
package/config.mjs
CHANGED
|
@@ -63,11 +63,36 @@ const SECTION_KEYS = [
|
|
|
63
63
|
"stageDir",
|
|
64
64
|
"assets",
|
|
65
65
|
"assetTransform",
|
|
66
|
+
"manifest",
|
|
67
|
+
"manifestFlags",
|
|
66
68
|
"clean",
|
|
67
69
|
"lang",
|
|
68
70
|
"deploy",
|
|
69
71
|
"release",
|
|
70
72
|
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Manifest keys a repository may **not** declare, because the build derives
|
|
76
|
+
* them and would only overwrite what was written.
|
|
77
|
+
*
|
|
78
|
+
* Silently overwriting is the failure this list exists to prevent: a
|
|
79
|
+
* `version` typed into the configuration would look authoritative, sit there
|
|
80
|
+
* unread, and disagree with the shipped package forever. Declaring one is an
|
|
81
|
+
* error naming the key and where the value actually comes from.
|
|
82
|
+
*
|
|
83
|
+
* @type {Readonly<Record<string, string>>}
|
|
84
|
+
*/
|
|
85
|
+
export const DERIVED_MANIFEST_KEYS = Object.freeze({
|
|
86
|
+
id: "`foundryPackage`, itself derived from package.json `name`",
|
|
87
|
+
version: "package.json `version`",
|
|
88
|
+
url: "package.json `repository`",
|
|
89
|
+
bugs: "package.json `repository`",
|
|
90
|
+
manifest: "package.json `repository` and the release tag",
|
|
91
|
+
download: "package.json `repository` and the release tag",
|
|
92
|
+
compatibility: "the top level of content-build.config.yaml",
|
|
93
|
+
relationships: "the top level of content-build.config.yaml",
|
|
94
|
+
packs: "the `packs` list at the top level of content-build.config.yaml",
|
|
95
|
+
});
|
|
71
96
|
const ASSET_KEYS = ["from", "to"];
|
|
72
97
|
const CLEAN_KEYS = ["extra"];
|
|
73
98
|
const LANG_KEYS = ["sources", "help"];
|
|
@@ -156,6 +181,41 @@ function normalizeAsset(value, index) {
|
|
|
156
181
|
});
|
|
157
182
|
}
|
|
158
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Validate the manifest specification.
|
|
186
|
+
*
|
|
187
|
+
* **Deliberately not key-checked.** Everything a repository declares here is
|
|
188
|
+
* emitted into the manifest unchanged, so a key Foundry adds in a later version
|
|
189
|
+
* can be declared without waiting for a release of this package. The only rule
|
|
190
|
+
* is the one that has a wrong answer rather than an unknown one: a key the
|
|
191
|
+
* build *derives* must not also be authored, because the authored value would
|
|
192
|
+
* be silently overwritten.
|
|
193
|
+
*
|
|
194
|
+
* That is also why it is its own block rather than being spread across
|
|
195
|
+
* `packageBuild:` directly — pass-through and unknown-key checking cannot
|
|
196
|
+
* coexist in one mapping, and the keys around it are worth checking.
|
|
197
|
+
*
|
|
198
|
+
* @param {unknown} value - The `manifest` block, or `undefined`.
|
|
199
|
+
* @returns {Readonly<Record<string, unknown>>} It, frozen; `{}` when absent.
|
|
200
|
+
*/
|
|
201
|
+
function normalizeManifest(value) {
|
|
202
|
+
if (value === undefined) return Object.freeze({});
|
|
203
|
+
if (!isMapping(value)) fail("packageBuild.manifest", "must be a mapping");
|
|
204
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
205
|
+
|
|
206
|
+
for (const [key, source] of Object.entries(DERIVED_MANIFEST_KEYS)) {
|
|
207
|
+
if (input[key] !== undefined) {
|
|
208
|
+
fail(
|
|
209
|
+
`packageBuild.manifest.${key}`,
|
|
210
|
+
`is derived from ${source} and must not be declared — it ` +
|
|
211
|
+
`would be overwritten, and the two would disagree with ` +
|
|
212
|
+
`nothing to say so`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return Object.freeze(structuredClone(input));
|
|
217
|
+
}
|
|
218
|
+
|
|
159
219
|
/**
|
|
160
220
|
* The resolved `packageBuild` section, every optional half filled in.
|
|
161
221
|
*
|
|
@@ -167,6 +227,11 @@ function normalizeAsset(value, index) {
|
|
|
167
227
|
* @property {string} stageDir The staged package root, relative to
|
|
168
228
|
* `rootDir`. Every asset `to:` lands under it.
|
|
169
229
|
* @property {readonly Readonly<AssetSpec>[]} assets
|
|
230
|
+
* @property {Readonly<Record<string, unknown>>} manifest The manifest
|
|
231
|
+
* specification, emitted as declared.
|
|
232
|
+
* @property {string|null} manifestFlags Module to load a `flags` function
|
|
233
|
+
* from, for namespaced flags a repository has
|
|
234
|
+
* to compute. `null` when it declares none.
|
|
170
235
|
* @property {string|null} assetTransform Module to load a `transform` from,
|
|
171
236
|
* resolved against `rootDir`. `null` when
|
|
172
237
|
* the repository stages assets verbatim.
|
|
@@ -280,6 +345,17 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
280
345
|
"packageBuild.assetTransform",
|
|
281
346
|
),
|
|
282
347
|
),
|
|
348
|
+
manifest: normalizeManifest(section.manifest),
|
|
349
|
+
manifestFlags:
|
|
350
|
+
section.manifestFlags === undefined ?
|
|
351
|
+
null
|
|
352
|
+
: path.resolve(
|
|
353
|
+
shared.rootDir,
|
|
354
|
+
requireNonEmptyString(
|
|
355
|
+
section.manifestFlags,
|
|
356
|
+
"packageBuild.manifestFlags",
|
|
357
|
+
),
|
|
358
|
+
),
|
|
283
359
|
cleanExtra: Object.freeze(cleanExtra),
|
|
284
360
|
langSources:
|
|
285
361
|
langInput.sources === undefined ?
|
package/manifest.mjs
CHANGED
|
@@ -15,26 +15,35 @@
|
|
|
15
15
|
* Building the Foundry package manifest — `system.json` or `module.json`.
|
|
16
16
|
*
|
|
17
17
|
* Foundry defines exactly two package kinds, and a repository is one of them,
|
|
18
|
-
* so there is one job here with two spellings:
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
18
|
+
* so there is one job here with two spellings: assemble the manifest from the
|
|
19
|
+
* repository's configuration and write it into the build stage.
|
|
20
|
+
*
|
|
21
|
+
* **There is no template any more.** A manifest used to be a hand-authored
|
|
22
|
+
* `system.template.json` that this module stamped a few fields into — which
|
|
23
|
+
* made it the one build input still written as JSON, by hand, per repository,
|
|
24
|
+
* with no schema and nothing checking it. Worse, it declared facts the
|
|
25
|
+
* configuration also declared: the pack list twice, in two formats, with
|
|
26
|
+
* nothing checking that the pairs agreed. `sohl-kethira-basic` hand-maintained
|
|
27
|
+
* its whole `module.json`, and its `download` named an older version than the
|
|
28
|
+
* module claimed.
|
|
29
|
+
*
|
|
30
|
+
* So the manifest is generated (#9). Three kinds of key end up in it:
|
|
31
|
+
*
|
|
32
|
+
* - **Declared** — the `packageBuild.manifest` block, emitted unchanged, so a
|
|
33
|
+
* key Foundry adds in a later version needs no release of this package.
|
|
34
|
+
* - **Derived** — the identity, the version, the release addresses, the
|
|
35
|
+
* compatibility ranges and the pack list. Declaring one of these is an error
|
|
36
|
+
* rather than an override: the authored copy would be silently overwritten.
|
|
37
|
+
* - **Computed** — namespaced `flags` a repository works out for itself.
|
|
29
38
|
*
|
|
30
39
|
* **Nothing here invents an address.** The repository URL is read from
|
|
31
|
-
* `package.json`'s `repository` field, normalised, and everything else
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
40
|
+
* `package.json`'s `repository` field, normalised, and everything else derived
|
|
41
|
+
* from it. A manifest advertising another package's URLs would send Foundry to
|
|
42
|
+
* the wrong release on every update check — exactly what a template copied
|
|
43
|
+
* between repositories produced.
|
|
35
44
|
*
|
|
36
45
|
* The rules are pure functions over data. I/O is confined to
|
|
37
|
-
* {@link
|
|
46
|
+
* {@link writeManifest}, which is the only export that touches disk.
|
|
38
47
|
*
|
|
39
48
|
* @module
|
|
40
49
|
*/
|
|
@@ -52,34 +61,6 @@ import path from "node:path";
|
|
|
52
61
|
*/
|
|
53
62
|
export const ARTIFACTS = Object.freeze(["system", "module"]);
|
|
54
63
|
|
|
55
|
-
/**
|
|
56
|
-
* Which artifact a template file builds.
|
|
57
|
-
*
|
|
58
|
-
* Inferred from the template's own name so the usual case takes no
|
|
59
|
-
* configuration: a repository that ships `system.template.json` is a system,
|
|
60
|
-
* and one that ships `module.template.json` is a module. That is the same pair
|
|
61
|
-
* `@heroiclands/content-build` resolves a package manifest from, so the two
|
|
62
|
-
* cannot disagree about what a repository is.
|
|
63
|
-
*
|
|
64
|
-
* @param {string} templatePath - Path to the manifest template.
|
|
65
|
-
* @returns {"system"|"module"} The artifact name.
|
|
66
|
-
* @throws {TypeError} When the name identifies neither kind — a template called
|
|
67
|
-
* something else leaves nothing to infer from, and guessing would silently
|
|
68
|
-
* emit a manifest Foundry never looks for.
|
|
69
|
-
*/
|
|
70
|
-
export function artifactFromTemplate(templatePath) {
|
|
71
|
-
const base = path.basename(String(templatePath ?? ""));
|
|
72
|
-
const artifact = ARTIFACTS.find((a) => base.startsWith(`${a}.`));
|
|
73
|
-
if (!artifact) {
|
|
74
|
-
throw new TypeError(
|
|
75
|
-
`Cannot tell whether "${base}" builds a system or a module. ` +
|
|
76
|
-
`Name it system.template.json or module.template.json, or pass ` +
|
|
77
|
-
`\`artifact\` explicitly.`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return artifact;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
64
|
/**
|
|
84
65
|
* The repository's web address, from whatever spelling `package.json` carries.
|
|
85
66
|
*
|
|
@@ -138,80 +119,155 @@ export function releaseUrls({ repoUrl, version, artifact }) {
|
|
|
138
119
|
}
|
|
139
120
|
|
|
140
121
|
/**
|
|
141
|
-
*
|
|
122
|
+
* The order the manifest's keys are written in.
|
|
142
123
|
*
|
|
143
|
-
*
|
|
124
|
+
* Foundry does not care, but a human reading a diff does, and the generated
|
|
125
|
+
* file has to be comparable against the hand-authored template it replaces —
|
|
126
|
+
* which is only possible if the order is fixed rather than incidental to which
|
|
127
|
+
* keys a repository happened to declare. Anything not listed keeps its declared
|
|
128
|
+
* order, after these.
|
|
144
129
|
*
|
|
145
|
-
*
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
* @type {readonly string[]}
|
|
131
|
+
*/
|
|
132
|
+
const MANIFEST_KEY_ORDER = Object.freeze([
|
|
133
|
+
"id",
|
|
134
|
+
"title",
|
|
135
|
+
"description",
|
|
136
|
+
"version",
|
|
137
|
+
"authors",
|
|
138
|
+
"license",
|
|
139
|
+
"readme",
|
|
140
|
+
"changelog",
|
|
141
|
+
"flags",
|
|
142
|
+
"compatibility",
|
|
143
|
+
"relationships",
|
|
144
|
+
"esmodules",
|
|
145
|
+
"styles",
|
|
146
|
+
"languages",
|
|
147
|
+
"documentTypes",
|
|
148
|
+
"packFolders",
|
|
149
|
+
"packs",
|
|
150
|
+
"media",
|
|
151
|
+
"socket",
|
|
152
|
+
"grid",
|
|
153
|
+
"primaryTokenAttribute",
|
|
154
|
+
"url",
|
|
155
|
+
"bugs",
|
|
156
|
+
"manifest",
|
|
157
|
+
"download",
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The manifest's `packs`, derived from the one pack list the build already has.
|
|
150
162
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
163
|
+
* The two used to be written separately — `content-build.config.yaml` declared
|
|
164
|
+
* a pack's name and type, and the manifest template declared them again beside
|
|
165
|
+
* a label, a path and a system id, with nothing checking that the pairs agreed.
|
|
166
|
+
* They are one list now.
|
|
167
|
+
*
|
|
168
|
+
* Companions are flattened in, because Foundry sees no difference: a companion
|
|
169
|
+
* is only a pack written by another pass rather than one of its own, and it
|
|
170
|
+
* ships as an ordinary compendium. The order matches `packDirectories`, so the
|
|
171
|
+
* manifest lists packs in the order the build compiles them.
|
|
172
|
+
*
|
|
173
|
+
* @param {object} config - The resolved content-build configuration.
|
|
174
|
+
* @returns {object[]} The manifest's `packs` array.
|
|
158
175
|
*/
|
|
159
|
-
export function
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
export function manifestPacks(config) {
|
|
177
|
+
const flatten = (pack) => [
|
|
178
|
+
pack,
|
|
179
|
+
...(pack.companions ?? []).flatMap(flatten),
|
|
180
|
+
];
|
|
181
|
+
return config.packs.flatMap(flatten).map((pack) => ({
|
|
182
|
+
label: pack.label,
|
|
183
|
+
type: pack.type,
|
|
184
|
+
name: pack.name,
|
|
185
|
+
system: config.stats.systemId,
|
|
186
|
+
path: `packs/${pack.name}`,
|
|
187
|
+
private: pack.private,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build a Foundry package manifest from the resolved configuration.
|
|
193
|
+
*
|
|
194
|
+
* Three kinds of key end up in the result:
|
|
195
|
+
*
|
|
196
|
+
* - **Declared** — everything in `packageBuild.manifest`, emitted unchanged, so
|
|
197
|
+
* a key Foundry adds later needs no release of this package.
|
|
198
|
+
* - **Derived** — the identity, the release addresses, the version, the Foundry
|
|
199
|
+
* and system compatibility ranges, and the pack list. These are refused if
|
|
200
|
+
* also declared: an authored copy would be overwritten and the two would
|
|
201
|
+
* disagree with nothing to say so.
|
|
202
|
+
* - **Computed** — namespaced `flags` a repository works out for itself, merged
|
|
203
|
+
* over any it declared.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} options - Inputs.
|
|
206
|
+
* @param {object} options.config - The resolved content-build configuration.
|
|
207
|
+
* @param {object} options.packageJson - The repository's `package.json`.
|
|
208
|
+
* @param {string} options.artifact - `system` or `module`.
|
|
209
|
+
* @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
|
|
210
|
+
* @returns {object} The manifest, ready to serialise.
|
|
211
|
+
*/
|
|
212
|
+
export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
213
|
+
const declared = config.packageBuild?.manifest ?? {};
|
|
214
|
+
const repoUrl = normalizeRepoUrl(packageJson.repository);
|
|
215
|
+
|
|
216
|
+
const derived = {
|
|
217
|
+
id: config.foundryPackage,
|
|
218
|
+
version: packageJson.version,
|
|
219
|
+
packs: manifestPacks(config),
|
|
220
|
+
...releaseUrls({ repoUrl, version: packageJson.version, artifact }),
|
|
164
221
|
};
|
|
222
|
+
if (config.compatibility) derived.compatibility = config.compatibility;
|
|
223
|
+
if (config.relationships && Object.keys(config.relationships).length) {
|
|
224
|
+
derived.relationships = config.relationships;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const merged = { ...declared, ...derived };
|
|
165
228
|
|
|
166
229
|
if (flags && Object.keys(flags).length) {
|
|
167
|
-
|
|
230
|
+
merged.flags = { ...(declared.flags ?? {}) };
|
|
168
231
|
for (const [namespace, values] of Object.entries(flags)) {
|
|
169
|
-
|
|
170
|
-
...(
|
|
232
|
+
merged.flags[namespace] = {
|
|
233
|
+
...(declared.flags?.[namespace] ?? {}),
|
|
171
234
|
...values,
|
|
172
235
|
};
|
|
173
236
|
}
|
|
174
237
|
}
|
|
175
238
|
|
|
176
|
-
|
|
239
|
+
// Ordered, so the generated file diffs against the template it replaces.
|
|
240
|
+
const ordered = {};
|
|
241
|
+
for (const key of MANIFEST_KEY_ORDER) {
|
|
242
|
+
if (merged[key] !== undefined) ordered[key] = merged[key];
|
|
243
|
+
}
|
|
244
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
245
|
+
if (!(key in ordered)) ordered[key] = value;
|
|
246
|
+
}
|
|
247
|
+
return ordered;
|
|
177
248
|
}
|
|
178
249
|
|
|
179
250
|
/**
|
|
180
|
-
*
|
|
251
|
+
* Write the generated manifest into the staged package.
|
|
181
252
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* @param {
|
|
186
|
-
* @param {string}
|
|
187
|
-
* @param {object}
|
|
188
|
-
*
|
|
189
|
-
* @param {string} opts.outDir - Directory to write the manifest into, created
|
|
190
|
-
* if absent.
|
|
191
|
-
* @param {"system"|"module"} [opts.artifact] - Overrides the artifact inferred
|
|
192
|
-
* from the template's name.
|
|
193
|
-
* @param {Record<string, object>} [opts.flags] - Namespaced flags to merge.
|
|
194
|
-
* @returns {Promise<{path: string, manifest: object}>} Where it was written,
|
|
195
|
-
* and what was written.
|
|
253
|
+
* @param {object} options - As {@link buildManifest}, plus where to write.
|
|
254
|
+
* @param {object} options.config - The resolved content-build configuration.
|
|
255
|
+
* @param {object} options.packageJson - The repository's `package.json`.
|
|
256
|
+
* @param {string} options.artifact - `system` or `module`.
|
|
257
|
+
* @param {string} options.outDir - Directory to write into.
|
|
258
|
+
* @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
|
|
259
|
+
* @returns {Promise<{path: string, manifest: object}>} Where it went, and what.
|
|
196
260
|
*/
|
|
197
|
-
export async function
|
|
198
|
-
|
|
261
|
+
export async function writeManifest({
|
|
262
|
+
config,
|
|
199
263
|
packageJson,
|
|
264
|
+
artifact,
|
|
200
265
|
outDir,
|
|
201
|
-
|
|
202
|
-
flags = undefined,
|
|
266
|
+
flags,
|
|
203
267
|
}) {
|
|
204
|
-
const
|
|
205
|
-
const template = JSON.parse(await fs.readFile(templatePath, "utf8"));
|
|
206
|
-
const manifest = stampManifest(template, {
|
|
207
|
-
version: packageJson.version,
|
|
208
|
-
repoUrl: normalizeRepoUrl(packageJson.repository),
|
|
209
|
-
artifact: kind,
|
|
210
|
-
flags,
|
|
211
|
-
});
|
|
212
|
-
|
|
268
|
+
const manifest = buildManifest({ config, packageJson, artifact, flags });
|
|
213
269
|
await fs.mkdir(outDir, { recursive: true });
|
|
214
|
-
const outPath = path.join(outDir, `${
|
|
270
|
+
const outPath = path.join(outDir, `${artifact}.json`);
|
|
215
271
|
// Trailing newline: the file is committed to a release archive and read by
|
|
216
272
|
// humans as often as by Foundry.
|
|
217
273
|
await fs.writeFile(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./index.mjs",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"stage.mjs",
|
|
56
56
|
"text.mjs",
|
|
57
57
|
"types",
|
|
58
|
+
"CHANGELOG.md",
|
|
58
59
|
"README.md",
|
|
59
60
|
"bin",
|
|
60
61
|
"config.mjs"
|
|
@@ -66,10 +67,12 @@
|
|
|
66
67
|
"build:types": "tsc -p tsconfig.dts.json",
|
|
67
68
|
"format": "prettier --write .",
|
|
68
69
|
"format:check": "prettier --check .",
|
|
70
|
+
"changeset": "changeset",
|
|
71
|
+
"changeset:check": "changeset status --since=origin/main",
|
|
69
72
|
"prepare": "git config core.hooksPath .githooks || true"
|
|
70
73
|
},
|
|
71
74
|
"dependencies": {
|
|
72
|
-
"@heroiclands/content-build": "^0.
|
|
75
|
+
"@heroiclands/content-build": "^1.0.0",
|
|
73
76
|
"acorn": "^8.18.0",
|
|
74
77
|
"archiver": "^8.0.0",
|
|
75
78
|
"dotenv": "^17.2.3",
|
|
@@ -78,6 +81,7 @@
|
|
|
78
81
|
"yargs": "^18.1.0"
|
|
79
82
|
},
|
|
80
83
|
"devDependencies": {
|
|
84
|
+
"@changesets/cli": "^3.0.0",
|
|
81
85
|
"@types/node": "^26.2.0",
|
|
82
86
|
"prettier": "^3.9.6",
|
|
83
87
|
"typescript": "^6.0.3",
|
package/types/config.d.mts
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* @property {string} stageDir The staged package root, relative to
|
|
10
10
|
* `rootDir`. Every asset `to:` lands under it.
|
|
11
11
|
* @property {readonly Readonly<AssetSpec>[]} assets
|
|
12
|
+
* @property {Readonly<Record<string, unknown>>} manifest The manifest
|
|
13
|
+
* specification, emitted as declared.
|
|
14
|
+
* @property {string|null} manifestFlags Module to load a `flags` function
|
|
15
|
+
* from, for namespaced flags a repository has
|
|
16
|
+
* to compute. `null` when it declares none.
|
|
12
17
|
* @property {string|null} assetTransform Module to load a `transform` from,
|
|
13
18
|
* resolved against `rootDir`. `null` when
|
|
14
19
|
* the repository stages assets verbatim.
|
|
@@ -44,6 +49,18 @@ export function resolvePackageBuildConfig(shared: object): Readonly<PackageBuild
|
|
|
44
49
|
* declares something malformed.
|
|
45
50
|
*/
|
|
46
51
|
export function loadPackageBuildConfig(): Readonly<PackageBuildConfig>;
|
|
52
|
+
/**
|
|
53
|
+
* Manifest keys a repository may **not** declare, because the build derives
|
|
54
|
+
* them and would only overwrite what was written.
|
|
55
|
+
*
|
|
56
|
+
* Silently overwriting is the failure this list exists to prevent: a
|
|
57
|
+
* `version` typed into the configuration would look authoritative, sit there
|
|
58
|
+
* unread, and disagree with the shipped package forever. Declaring one is an
|
|
59
|
+
* error naming the key and where the value actually comes from.
|
|
60
|
+
*
|
|
61
|
+
* @type {Readonly<Record<string, string>>}
|
|
62
|
+
*/
|
|
63
|
+
export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
|
|
47
64
|
/**
|
|
48
65
|
* One staging copy: a source path in the repository, and where it lands under
|
|
49
66
|
* the staged package root.
|
|
@@ -84,6 +101,17 @@ export type PackageBuildConfig = {
|
|
|
84
101
|
*/
|
|
85
102
|
stageDir: string;
|
|
86
103
|
assets: readonly Readonly<AssetSpec>[];
|
|
104
|
+
/**
|
|
105
|
+
* The manifest
|
|
106
|
+
* specification, emitted as declared.
|
|
107
|
+
*/
|
|
108
|
+
manifest: Readonly<Record<string, unknown>>;
|
|
109
|
+
/**
|
|
110
|
+
* Module to load a `flags` function
|
|
111
|
+
* from, for namespaced flags a repository has
|
|
112
|
+
* to compute. `null` when it declares none.
|
|
113
|
+
*/
|
|
114
|
+
manifestFlags: string | null;
|
|
87
115
|
/**
|
|
88
116
|
* Module to load a `transform` from,
|
|
89
117
|
* resolved against `rootDir`. `null` when
|
package/types/manifest.d.mts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which artifact a template file builds.
|
|
3
|
-
*
|
|
4
|
-
* Inferred from the template's own name so the usual case takes no
|
|
5
|
-
* configuration: a repository that ships `system.template.json` is a system,
|
|
6
|
-
* and one that ships `module.template.json` is a module. That is the same pair
|
|
7
|
-
* `@heroiclands/content-build` resolves a package manifest from, so the two
|
|
8
|
-
* cannot disagree about what a repository is.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} templatePath - Path to the manifest template.
|
|
11
|
-
* @returns {"system"|"module"} The artifact name.
|
|
12
|
-
* @throws {TypeError} When the name identifies neither kind — a template called
|
|
13
|
-
* something else leaves nothing to infer from, and guessing would silently
|
|
14
|
-
* emit a manifest Foundry never looks for.
|
|
15
|
-
*/
|
|
16
|
-
export function artifactFromTemplate(templatePath: string): "system" | "module";
|
|
17
1
|
/**
|
|
18
2
|
* The repository's web address, from whatever spelling `package.json` carries.
|
|
19
3
|
*
|
|
@@ -59,53 +43,65 @@ export function releaseUrls({ repoUrl, version, artifact }: {
|
|
|
59
43
|
download: string;
|
|
60
44
|
};
|
|
61
45
|
/**
|
|
62
|
-
*
|
|
46
|
+
* The manifest's `packs`, derived from the one pack list the build already has.
|
|
63
47
|
*
|
|
64
|
-
*
|
|
48
|
+
* The two used to be written separately — `content-build.config.yaml` declared
|
|
49
|
+
* a pack's name and type, and the manifest template declared them again beside
|
|
50
|
+
* a label, a path and a system id, with nothing checking that the pairs agreed.
|
|
51
|
+
* They are one list now.
|
|
65
52
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* a Foundry package.
|
|
53
|
+
* Companions are flattened in, because Foundry sees no difference: a companion
|
|
54
|
+
* is only a pack written by another pass rather than one of its own, and it
|
|
55
|
+
* ships as an ordinary compendium. The order matches `packDirectories`, so the
|
|
56
|
+
* manifest lists packs in the order the build compiles them.
|
|
71
57
|
*
|
|
72
|
-
* @param {object}
|
|
73
|
-
* @
|
|
74
|
-
* @param {string} opts.version - The version being built.
|
|
75
|
-
* @param {string} opts.repoUrl - Normalised repository URL.
|
|
76
|
-
* @param {"system"|"module"} opts.artifact - Which artifact is shipped.
|
|
77
|
-
* @param {Record<string, object>} [opts.flags] - Namespaced flags to merge.
|
|
78
|
-
* @returns {object} The stamped manifest.
|
|
58
|
+
* @param {object} config - The resolved content-build configuration.
|
|
59
|
+
* @returns {object[]} The manifest's `packs` array.
|
|
79
60
|
*/
|
|
80
|
-
export function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
export function manifestPacks(config: object): object[];
|
|
62
|
+
/**
|
|
63
|
+
* Build a Foundry package manifest from the resolved configuration.
|
|
64
|
+
*
|
|
65
|
+
* Three kinds of key end up in the result:
|
|
66
|
+
*
|
|
67
|
+
* - **Declared** — everything in `packageBuild.manifest`, emitted unchanged, so
|
|
68
|
+
* a key Foundry adds later needs no release of this package.
|
|
69
|
+
* - **Derived** — the identity, the release addresses, the version, the Foundry
|
|
70
|
+
* and system compatibility ranges, and the pack list. These are refused if
|
|
71
|
+
* also declared: an authored copy would be overwritten and the two would
|
|
72
|
+
* disagree with nothing to say so.
|
|
73
|
+
* - **Computed** — namespaced `flags` a repository works out for itself, merged
|
|
74
|
+
* over any it declared.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} options - Inputs.
|
|
77
|
+
* @param {object} options.config - The resolved content-build configuration.
|
|
78
|
+
* @param {object} options.packageJson - The repository's `package.json`.
|
|
79
|
+
* @param {string} options.artifact - `system` or `module`.
|
|
80
|
+
* @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
|
|
81
|
+
* @returns {object} The manifest, ready to serialise.
|
|
82
|
+
*/
|
|
83
|
+
export function buildManifest({ config, packageJson, artifact, flags }: {
|
|
84
|
+
config: object;
|
|
85
|
+
packageJson: object;
|
|
86
|
+
artifact: string;
|
|
84
87
|
flags?: Record<string, object> | undefined;
|
|
85
88
|
}): object;
|
|
86
89
|
/**
|
|
87
|
-
*
|
|
90
|
+
* Write the generated manifest into the staged package.
|
|
88
91
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* @param {
|
|
93
|
-
* @param {string}
|
|
94
|
-
* @param {object}
|
|
95
|
-
*
|
|
96
|
-
* @param {string} opts.outDir - Directory to write the manifest into, created
|
|
97
|
-
* if absent.
|
|
98
|
-
* @param {"system"|"module"} [opts.artifact] - Overrides the artifact inferred
|
|
99
|
-
* from the template's name.
|
|
100
|
-
* @param {Record<string, object>} [opts.flags] - Namespaced flags to merge.
|
|
101
|
-
* @returns {Promise<{path: string, manifest: object}>} Where it was written,
|
|
102
|
-
* and what was written.
|
|
92
|
+
* @param {object} options - As {@link buildManifest}, plus where to write.
|
|
93
|
+
* @param {object} options.config - The resolved content-build configuration.
|
|
94
|
+
* @param {object} options.packageJson - The repository's `package.json`.
|
|
95
|
+
* @param {string} options.artifact - `system` or `module`.
|
|
96
|
+
* @param {string} options.outDir - Directory to write into.
|
|
97
|
+
* @param {Record<string, object>} [options.flags] - Namespaced flags to merge.
|
|
98
|
+
* @returns {Promise<{path: string, manifest: object}>} Where it went, and what.
|
|
103
99
|
*/
|
|
104
|
-
export function
|
|
105
|
-
|
|
100
|
+
export function writeManifest({ config, packageJson, artifact, outDir, flags, }: {
|
|
101
|
+
config: object;
|
|
106
102
|
packageJson: object;
|
|
103
|
+
artifact: string;
|
|
107
104
|
outDir: string;
|
|
108
|
-
artifact?: "module" | "system" | undefined;
|
|
109
105
|
flags?: Record<string, object> | undefined;
|
|
110
106
|
}): Promise<{
|
|
111
107
|
path: string;
|