@heroiclands/package-build 3.0.0 → 3.1.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 +81 -0
- package/CONTENT.md +17 -0
- package/MIGRATING.md +4 -4
- package/README.md +8 -1
- package/config.mjs +43 -2
- package/container.mjs +17 -6
- package/content-config.mjs +69 -1
- package/engine/compendiums.mjs +28 -8
- package/engine/pack-config.mjs +1 -1
- package/manifest.mjs +17 -8
- package/package.json +1 -1
- package/types/config.d.mts +9 -0
- package/types/container.d.mts +15 -4
- package/types/content-config.d.mts +16 -0
- package/types/engine/pack-config.d.mts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 40386b1: Build a pack from JSON that is already built, and declare the system per pack.
|
|
8
|
+
|
|
9
|
+
The compile is two stages — content notes to `build/packs-json/<pack>/`, then
|
|
10
|
+
that directory to LevelDB — but only the first could feed the second. A package
|
|
11
|
+
whose packs are already Foundry JSON had no way in: generation runs first and
|
|
12
|
+
refuses an empty content tree, so a package with no `assets/content` threw
|
|
13
|
+
before the compile loop, and staging the files by hand did not survive
|
|
14
|
+
`generatePack`'s `rmSync` of its destination.
|
|
15
|
+
|
|
16
|
+
`packs[].prebuilt` names the directory a pack's per-document JSON already lives
|
|
17
|
+
in. Generation is skipped for it and the compile reads from there, so
|
|
18
|
+
`cleanPackEntry` and the Scene/Level integrity check still run — which is the
|
|
19
|
+
reason to route through this toolchain rather than call `compilePack` directly.
|
|
20
|
+
When every selected pack is prebuilt the content walk is skipped altogether.
|
|
21
|
+
|
|
22
|
+
`prebuilt` may not be combined with `folders`, `companions` or `default`, and
|
|
23
|
+
may not be declared on a companion. Each of those describes a generation pass,
|
|
24
|
+
and a prebuilt pack has none; refusing is better than ignoring a folder file
|
|
25
|
+
that can never be read.
|
|
26
|
+
|
|
27
|
+
Separately, `stats.systemId` is now optional and `packs[].system` declares it
|
|
28
|
+
per pack, falling back to the package-wide value and omitted from the manifest
|
|
29
|
+
when neither is set. Every pack used to be emitted with one system id, which no
|
|
30
|
+
package needing two could express. Foundry requires `system` on ActiveEffect,
|
|
31
|
+
Actor and Item packs and on no others, and an Adventure pack that declares one
|
|
32
|
+
is hidden from every other system.
|
|
33
|
+
- de6dc40: Let packages share one Foundry container, and so one signed licence.
|
|
34
|
+
|
|
35
|
+
The container name was derived from the package id, and `--hostname` set to
|
|
36
|
+
match. The hostname part is right — Foundry binds a signed licence to it, and a
|
|
37
|
+
stable one is exactly what makes the signature survive a `recreate`. What was
|
|
38
|
+
wrong is that the value could not be shared: `sohl` got `sohl-foundry-test` and
|
|
39
|
+
`hm3` got `hm3-foundry-test`, so a `Config/license.json` signed for the first
|
|
40
|
+
would not verify for the second, and one maintainer with one dev licence needed
|
|
41
|
+
one per package.
|
|
42
|
+
|
|
43
|
+
Neither fallback rescues it. Passing `FOUNDRYVTT_<STAGE>_LICENSE_KEY` makes the
|
|
44
|
+
felddy image write the key **unsigned**, and Foundry v13+ refuses to start with
|
|
45
|
+
`Software license requires signature`; omitting it makes the image fetch a key
|
|
46
|
+
from the account, unsigned, same refusal. Signing is a one-time interactive step
|
|
47
|
+
per host, so a second package's container could not come up without a second
|
|
48
|
+
licence — or a re-signing that then broke the first.
|
|
49
|
+
|
|
50
|
+
The rest of the shared-instance model already worked. `requireIsolatedDataRoot`
|
|
51
|
+
refuses only the dev/qa/prod roots, so a shared **test** root was already
|
|
52
|
+
allowed, and `resolveE2EWorld` already derives a distinct world id per package,
|
|
53
|
+
so one data root already holds both systems and both worlds with `FOUNDRY_WORLD`
|
|
54
|
+
choosing which launches. The container identity was the last package-scoped
|
|
55
|
+
piece.
|
|
56
|
+
|
|
57
|
+
So `packageBuild.container.name` declares it:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
packageBuild:
|
|
61
|
+
container:
|
|
62
|
+
# Shared with the other HeroicLands packages so one signed Foundry
|
|
63
|
+
# licence covers them all.
|
|
64
|
+
name: heroiclands-foundry
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The stage is still appended — this declares `heroiclands-foundry-test`, not
|
|
68
|
+
`heroiclands-foundry`. Sharing is meant to cross packages, not stages: two
|
|
69
|
+
stages are two containers over two data roots, and docker names are unique, so a
|
|
70
|
+
name used whole would have `container dev` find the `test` container already
|
|
71
|
+
there, start it, and serve the test data root on the dev port. Nothing is
|
|
72
|
+
declared by default, and the name stays `<packageId>-foundry-<stage>`.
|
|
73
|
+
|
|
74
|
+
## 3.0.1
|
|
75
|
+
|
|
76
|
+
### Patch Changes
|
|
77
|
+
|
|
78
|
+
- 73a7e60: Correct the release the merge is dated to: 3.0.0, not 2.0.0.
|
|
79
|
+
|
|
80
|
+
The merge commit set `version` by hand _and_ carried a major changeset, so
|
|
81
|
+
changesets bumped it a second time. `MIGRATING.md` was the live defect — it told
|
|
82
|
+
a consumer to install `^2.0.0`, which resolves nowhere.
|
|
83
|
+
|
|
3
84
|
## 3.0.0
|
|
4
85
|
|
|
5
86
|
### Major Changes
|
package/CONTENT.md
CHANGED
|
@@ -86,6 +86,23 @@ packs:
|
|
|
86
86
|
companions:
|
|
87
87
|
- { name: adventures, type: Adventure }
|
|
88
88
|
|
|
89
|
+
# A pack whose per-document JSON is already built — checked in rather than
|
|
90
|
+
# generated. `prebuilt` names where it lives, generation is skipped for it,
|
|
91
|
+
# and `cleanPackEntry` and the Scene/Level integrity check still run. It may
|
|
92
|
+
# not carry `folders`, `companions` or `default`, and may not be a companion:
|
|
93
|
+
# each of those describes a generation pass a prebuilt pack does not have.
|
|
94
|
+
# When every configured pack is prebuilt the content walk is skipped
|
|
95
|
+
# entirely, so a package with no `assets/content` builds.
|
|
96
|
+
- name: adventures
|
|
97
|
+
type: Adventure
|
|
98
|
+
prebuilt: assets/packs/adventure
|
|
99
|
+
# Foundry requires `system` on ActiveEffect, Actor and Item packs and on no
|
|
100
|
+
# others, so it is declared per pack. Unset falls back to `stats.systemId`
|
|
101
|
+
# — itself optional — and with neither the manifest omits the key. An
|
|
102
|
+
# Adventure or Scene pack that names a system is hidden from every other
|
|
103
|
+
# one, which is rarely what a package that declined to name one meant.
|
|
104
|
+
system: null
|
|
105
|
+
|
|
89
106
|
# How this repository frames the pages `content-build docs` generates. The
|
|
90
107
|
# tables come from the itemBuilders registry and are the same everywhere; the
|
|
91
108
|
# heading, the filing and what a reader is told first are this repository's.
|
package/MIGRATING.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Migrating to `@heroiclands/package-build`
|
|
1
|
+
# Migrating to `@heroiclands/package-build` 3.0.0
|
|
2
2
|
|
|
3
3
|
`@heroiclands/content-build` and `@heroiclands/package-build` are one package.
|
|
4
|
-
The content half now ships inside `@heroiclands/package-build` at
|
|
4
|
+
The content half now ships inside `@heroiclands/package-build` at 3.0.0;
|
|
5
5
|
`@heroiclands/content-build` is deprecated and receives no further releases.
|
|
6
6
|
|
|
7
7
|
Nothing about how a build _works_ changed. This is a packaging change: the same
|
|
@@ -22,13 +22,13 @@ that touched a single idea.
|
|
|
22
22
|
|
|
23
23
|
## 1. Dependencies
|
|
24
24
|
|
|
25
|
-
Drop `@heroiclands/content-build` and move to
|
|
25
|
+
Drop `@heroiclands/content-build` and move to 3.0.0:
|
|
26
26
|
|
|
27
27
|
```diff
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
- "@heroiclands/content-build": "^1.8.2",
|
|
30
30
|
- "@heroiclands/package-build": "^0.6.1"
|
|
31
|
-
+ "@heroiclands/package-build": "^
|
|
31
|
+
+ "@heroiclands/package-build": "^3.0.0"
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The content half is documented separately in **[CONTENT.md](CONTENT.md)** — th
|
|
|
14
14
|
note format, the pack pipeline, and the configuration contract a content tree
|
|
15
15
|
declares itself with.
|
|
16
16
|
|
|
17
|
-
> **This package was two.** Until
|
|
17
|
+
> **This package was two.** Until 3.0.0 the content half shipped as
|
|
18
18
|
> `@heroiclands/content-build`. No consumer ever installed one without the
|
|
19
19
|
> other, and the packaging half depended on the content half besides, so the
|
|
20
20
|
> boundary bought nothing and cost a configuration file with two owners and a
|
|
@@ -189,6 +189,13 @@ packageBuild:
|
|
|
189
189
|
# `FOUNDRYVTT_<STAGE>_DATA`, the same variable `deploy` writes into, and their
|
|
190
190
|
# ports are conventional. Declare a stage only when it is genuinely yours.
|
|
191
191
|
container:
|
|
192
|
+
# Optional. The container name, minus the stage, which is always appended:
|
|
193
|
+
# this declares `heroiclands-foundry-test`, not `sohl-foundry-test`.
|
|
194
|
+
# Foundry binds a signed licence to the container hostname, so packages that
|
|
195
|
+
# declare the same name are one instance covered by one signature. Defaults
|
|
196
|
+
# to the package id, which keeps every package in its own container.
|
|
197
|
+
name: heroiclands-foundry
|
|
198
|
+
|
|
192
199
|
stages:
|
|
193
200
|
# SoHL keeps the previous, pre-TypeScript system on an older Foundry.
|
|
194
201
|
# An empty `world` declares "never auto-launch" — it is managed by hand.
|
package/config.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* loader checks that the section is a mapping and hands it back frozen;
|
|
30
30
|
* everything inside it is validated here.
|
|
31
31
|
*
|
|
32
|
-
* **That section used to be a reservation.** Until
|
|
32
|
+
* **That section used to be a reservation.** Until 3.0.0 these were two
|
|
33
33
|
* packages, and `packageBuild:` was a block `@heroiclands/content-build`
|
|
34
34
|
* carried on behalf of a toolchain it knew nothing about. One package now owns
|
|
35
35
|
* the whole file, so it is an ordinary section — but the validation split is
|
|
@@ -114,7 +114,16 @@ const LANG_KEYS = [
|
|
|
114
114
|
];
|
|
115
115
|
const DEPLOY_KEYS = ["envPrefix"];
|
|
116
116
|
const RELEASE_KEYS = ["artifact"];
|
|
117
|
-
const CONTAINER_KEYS = ["image", "stages"];
|
|
117
|
+
const CONTAINER_KEYS = ["image", "name", "stages"];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What docker accepts as the head of a container name.
|
|
121
|
+
*
|
|
122
|
+
* Checked here rather than left to `docker run`, because the stage is appended
|
|
123
|
+
* to it: a rejected name surfaces as a failure to create a container whose name
|
|
124
|
+
* the repository never wrote down.
|
|
125
|
+
*/
|
|
126
|
+
const CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
118
127
|
const CONTAINER_STAGE_KEYS = ["port", "world", "version"];
|
|
119
128
|
const E2E_KEYS = ["stage", "suite", "build", "world", "gm", "documents"];
|
|
120
129
|
const E2E_SUITE_KEYS = ["run", "open"];
|
|
@@ -265,6 +274,30 @@ function normalizeGlobs(value, fallback, where) {
|
|
|
265
274
|
);
|
|
266
275
|
}
|
|
267
276
|
|
|
277
|
+
/**
|
|
278
|
+
* A container name several packages may share.
|
|
279
|
+
*
|
|
280
|
+
* Foundry binds a signed licence to the container hostname, and the hostname
|
|
281
|
+
* follows the name. Declaring the same one in two repositories is how they come
|
|
282
|
+
* to be one instance covered by one signature — so this is a name that must
|
|
283
|
+
* survive being retyped elsewhere, and the stage is still appended to it.
|
|
284
|
+
*
|
|
285
|
+
* @param {unknown} value - The declared name.
|
|
286
|
+
* @returns {string} The name, minus the stage.
|
|
287
|
+
*/
|
|
288
|
+
function requireContainerName(value) {
|
|
289
|
+
const where = "packageBuild.container.name";
|
|
290
|
+
const name = requireNonEmptyString(value, where);
|
|
291
|
+
if (!CONTAINER_NAME.test(name)) {
|
|
292
|
+
fail(
|
|
293
|
+
where,
|
|
294
|
+
"must be a container name docker accepts — a letter or digit, " +
|
|
295
|
+
"then letters, digits, underscores, periods or hyphens",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return name;
|
|
299
|
+
}
|
|
300
|
+
|
|
268
301
|
/**
|
|
269
302
|
* A container stage a repository runs beyond the conventional four.
|
|
270
303
|
*
|
|
@@ -477,6 +510,9 @@ function normalizeExceptions(value, field, where) {
|
|
|
477
510
|
* @property {string|null} systemVersion That system's version, when the shared
|
|
478
511
|
* configuration stamps one.
|
|
479
512
|
* @property {string|null} containerImage Image override for every stage.
|
|
513
|
+
* @property {string|null} containerName Container name, minus the stage,
|
|
514
|
+
* shared with the packages that declare the same one so a single signed
|
|
515
|
+
* Foundry licence covers them all. `null` names it after the package.
|
|
480
516
|
* @property {Readonly<Record<string, Readonly<{port: number|null, world: string|null, version: string|null}>>>} containerStages
|
|
481
517
|
* Container stages beyond the conventional four.
|
|
482
518
|
* @property {string} e2eStage Which stage the suite runs against.
|
|
@@ -575,6 +611,10 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
575
611
|
"packageBuild.container.",
|
|
576
612
|
);
|
|
577
613
|
const containerInput = /** @type {Record<string, unknown>} */ (container);
|
|
614
|
+
const containerName =
|
|
615
|
+
containerInput.name === undefined ?
|
|
616
|
+
null
|
|
617
|
+
: requireContainerName(containerInput.name);
|
|
578
618
|
const declaredStages = containerInput.stages ?? {};
|
|
579
619
|
if (!isMapping(declaredStages)) {
|
|
580
620
|
fail("packageBuild.container.stages", "must be a mapping");
|
|
@@ -765,6 +805,7 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
765
805
|
containerInput.image,
|
|
766
806
|
"packageBuild.container.image",
|
|
767
807
|
),
|
|
808
|
+
containerName,
|
|
768
809
|
containerStages,
|
|
769
810
|
e2eStage:
|
|
770
811
|
e2eInput.stage === undefined ?
|
package/container.mjs
CHANGED
|
@@ -110,16 +110,27 @@ export function dataEnvVar(stage) {
|
|
|
110
110
|
/**
|
|
111
111
|
* The container name for a package's stage.
|
|
112
112
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
113
|
+
* Stable, so Foundry's signed licence — which is bound to the container
|
|
114
|
+
* hostname — survives a recreate. Named after the package by default, so two
|
|
115
|
+
* HeroicLands packages run their own containers side by side.
|
|
116
|
+
*
|
|
117
|
+
* A declared `base` replaces the package-scoped half, which is what lets
|
|
118
|
+
* several packages share one container and so one signed licence: a signature
|
|
119
|
+
* is issued for a hostname, not for a package, and a package that cannot name
|
|
120
|
+
* the host cannot be pointed at a signature that already exists.
|
|
121
|
+
*
|
|
122
|
+
* The stage stays in the name either way. Two stages are two containers over
|
|
123
|
+
* two data roots, and docker names are unique: were a declared name used
|
|
124
|
+
* whole, `container dev` would find the `test` container already there, start
|
|
125
|
+
* it, and serve the test data root on the dev port.
|
|
116
126
|
*
|
|
117
127
|
* @param {string} packageId - The Foundry package id.
|
|
118
128
|
* @param {string} stage - The stage name.
|
|
129
|
+
* @param {string|null} [base] - Declared name, shareable between packages.
|
|
119
130
|
* @returns {string} The container name.
|
|
120
131
|
*/
|
|
121
|
-
export function containerName(packageId, stage) {
|
|
122
|
-
return `${packageId}-foundry-${stage}`;
|
|
132
|
+
export function containerName(packageId, stage, base = null) {
|
|
133
|
+
return `${base ?? `${packageId}-foundry`}-${stage}`;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
/**
|
|
@@ -522,7 +533,7 @@ export function resolveContainer({ stage, config, env = process.env }) {
|
|
|
522
533
|
const port = resolveStagePort(stage, { env, stages });
|
|
523
534
|
return {
|
|
524
535
|
stage,
|
|
525
|
-
name: containerName(config.packageId, stage),
|
|
536
|
+
name: containerName(config.packageId, stage, config.containerName),
|
|
526
537
|
port,
|
|
527
538
|
image: resolveImage({
|
|
528
539
|
env,
|
package/content-config.mjs
CHANGED
|
@@ -181,6 +181,14 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
181
181
|
* @property {string|null} [folders] The pack's folder-hierarchy file, relative
|
|
182
182
|
* to `paths.content`. Default `null` — no
|
|
183
183
|
* folder documents are emitted.
|
|
184
|
+
* @property {string} [prebuilt] Directory holding this pack's per-document
|
|
185
|
+
* JSON, already built. Declaring it skips
|
|
186
|
+
* generation for the pack and compiles from
|
|
187
|
+
* there instead.
|
|
188
|
+
* @property {string} [system] The system this pack depends on, written
|
|
189
|
+
* to the manifest. Defaults to
|
|
190
|
+
* `stats.systemId`; omitted when neither is
|
|
191
|
+
* set.
|
|
184
192
|
* @property {PackSpec[]} [companions] Packs written by this pack's own compiler
|
|
185
193
|
* pass rather than a pass of their own (the
|
|
186
194
|
* scenes pass also emits the adventures
|
|
@@ -208,6 +216,8 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
|
|
|
208
216
|
* @property {string} label
|
|
209
217
|
* @property {boolean} private
|
|
210
218
|
* @property {string|null} folders
|
|
219
|
+
* @property {string|null} prebuilt
|
|
220
|
+
* @property {string|null} system
|
|
211
221
|
* @property {readonly Readonly<ResolvedPackSpec>[]} companions
|
|
212
222
|
* @property {boolean} mayBeEmpty
|
|
213
223
|
* @property {boolean} default
|
|
@@ -546,6 +556,8 @@ const PACK_KEYS = [
|
|
|
546
556
|
"companions",
|
|
547
557
|
"mayBeEmpty",
|
|
548
558
|
"default",
|
|
559
|
+
"prebuilt",
|
|
560
|
+
"system",
|
|
549
561
|
];
|
|
550
562
|
const PATH_KEYS = Object.keys(DEFAULT_PATHS);
|
|
551
563
|
const STATS_KEYS = ["systemId", "systemVersion", "lastModifiedBy"];
|
|
@@ -673,6 +685,54 @@ function normalizePack(value, where, nested = false) {
|
|
|
673
685
|
normalizePack(companion, `${where}.companions[${index}]`, true),
|
|
674
686
|
);
|
|
675
687
|
|
|
688
|
+
// A prebuilt pack's per-document JSON already exists, so it has no
|
|
689
|
+
// generation pass. Every key below describes one, which is why none of them
|
|
690
|
+
// may accompany it: silently ignoring a `folders` file that can never be
|
|
691
|
+
// read is worse than refusing the configuration that declares it.
|
|
692
|
+
const prebuilt =
|
|
693
|
+
pack.prebuilt === undefined || pack.prebuilt === null ?
|
|
694
|
+
null
|
|
695
|
+
: requireNonEmptyString(pack.prebuilt, `${where}.prebuilt`);
|
|
696
|
+
if (prebuilt !== null) {
|
|
697
|
+
if (nested) {
|
|
698
|
+
fail(
|
|
699
|
+
`${where}.prebuilt`,
|
|
700
|
+
"may not be declared on a companion: a companion is written by " +
|
|
701
|
+
"another pack's pass, and a prebuilt pack has no pass",
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (pack.folders !== undefined && pack.folders !== null) {
|
|
705
|
+
fail(
|
|
706
|
+
`${where}.folders`,
|
|
707
|
+
"may not accompany `prebuilt`: the folder hierarchy is built " +
|
|
708
|
+
"during generation, which a prebuilt pack skips",
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
if (Array.isArray(companionsInput) && companionsInput.length) {
|
|
712
|
+
fail(
|
|
713
|
+
`${where}.companions`,
|
|
714
|
+
"may not accompany `prebuilt`: a companion is written by this " +
|
|
715
|
+
"pack's pass, and a prebuilt pack has none",
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
if (pack.default === true) {
|
|
719
|
+
fail(
|
|
720
|
+
`${where}.default`,
|
|
721
|
+
"may not accompany `prebuilt`: the default pack receives notes " +
|
|
722
|
+
"declaring no `pack:`, and no note is routed into a prebuilt one",
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Foundry requires `system` on ActiveEffect, Actor and Item packs and on no
|
|
728
|
+
// others (CONST.SYSTEM_SPECIFIC_COMPENDIUM_TYPES), so a package may need a
|
|
729
|
+
// different answer per pack. Unset here falls back to `stats.systemId`, and
|
|
730
|
+
// unset in both omits the key from the manifest.
|
|
731
|
+
const system =
|
|
732
|
+
pack.system === undefined || pack.system === null ?
|
|
733
|
+
null
|
|
734
|
+
: requireNonEmptyString(pack.system, `${where}.system`);
|
|
735
|
+
|
|
676
736
|
/** @type {ResolvedPackSpec} */
|
|
677
737
|
const normalized = {
|
|
678
738
|
name,
|
|
@@ -695,6 +755,8 @@ function normalizePack(value, where, nested = false) {
|
|
|
695
755
|
// Which pack of a type receives a note that declares none. Validated
|
|
696
756
|
// across the whole list in `defineConfig` — at most one per type.
|
|
697
757
|
default: optionalBoolean(pack.default, `${where}.default`, false),
|
|
758
|
+
prebuilt,
|
|
759
|
+
system,
|
|
698
760
|
};
|
|
699
761
|
return Object.freeze(normalized);
|
|
700
762
|
}
|
|
@@ -747,7 +809,13 @@ function normalizeStats(value) {
|
|
|
747
809
|
rejectUnknownKeys(input, STATS_KEYS, "stats.");
|
|
748
810
|
|
|
749
811
|
return Object.freeze({
|
|
750
|
-
|
|
812
|
+
// Optional: a package whose packs are not all for one system declares
|
|
813
|
+
// the system per pack instead (`packs[].system`), and a package that
|
|
814
|
+
// ships only system-agnostic documents declares none at all.
|
|
815
|
+
systemId:
|
|
816
|
+
input.systemId === undefined || input.systemId === null ?
|
|
817
|
+
null
|
|
818
|
+
: requireNonEmptyString(input.systemId, "stats.systemId"),
|
|
751
819
|
systemVersion: requireNonEmptyString(
|
|
752
820
|
input.systemVersion,
|
|
753
821
|
"stats.systemVersion",
|
package/engine/compendiums.mjs
CHANGED
|
@@ -91,21 +91,41 @@ export async function compilePacks({
|
|
|
91
91
|
(name) => !packName || name === packName,
|
|
92
92
|
);
|
|
93
93
|
|
|
94
|
+
// A prebuilt pack's JSON is checked in rather than generated, so it is
|
|
95
|
+
// compiled from where it lives. Anything else comes from the build-only
|
|
96
|
+
// intermediate the generation pass writes.
|
|
97
|
+
const prebuiltDirs = new Map(
|
|
98
|
+
config.packs
|
|
99
|
+
.filter((pack) => pack.prebuilt)
|
|
100
|
+
.map((pack) => [pack.name, pack.prebuilt]),
|
|
101
|
+
);
|
|
102
|
+
const needsGeneration = packNames.some((name) => !prebuiltDirs.has(name));
|
|
103
|
+
|
|
94
104
|
// Generate the per-entry JSON from the content tree into the build-only
|
|
95
105
|
// JSON intermediate. The package-id guard runs first, inside this call,
|
|
96
106
|
// before any pack is written.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
107
|
+
//
|
|
108
|
+
// Skipped when every selected pack is prebuilt: generation refuses an empty
|
|
109
|
+
// content tree, and a package that ships only prebuilt packs has none at
|
|
110
|
+
// all — so running it would fail the build over a tree nothing reads.
|
|
111
|
+
if (needsGeneration) {
|
|
112
|
+
const errors = await generatePacksJson({ only: packName, config });
|
|
113
|
+
if (errors > 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Pack JSON generation reported ${errors} error(s); refusing to ` +
|
|
116
|
+
`compile packs from incomplete output.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
for (const name of packNames) {
|
|
106
|
-
const source = packJsonDir(name, config);
|
|
122
|
+
const source = prebuiltDirs.get(name) ?? packJsonDir(name, config);
|
|
107
123
|
if (!fs.existsSync(source)) {
|
|
108
|
-
log.error(
|
|
124
|
+
log.error(
|
|
125
|
+
prebuiltDirs.has(name) ?
|
|
126
|
+
`Pack ${name}: prebuilt JSON not found at ${source}.`
|
|
127
|
+
: `Pack ${name}: generated JSON not found at ${source}.`,
|
|
128
|
+
);
|
|
109
129
|
continue;
|
|
110
130
|
}
|
|
111
131
|
|
package/engine/pack-config.mjs
CHANGED
|
@@ -94,7 +94,7 @@ export const CONFIG_BASENAME = "package-build.config";
|
|
|
94
94
|
* reach for them: YAML first, `.mjs` last.
|
|
95
95
|
*
|
|
96
96
|
* The `content-build.config.*` stem this package read before the two toolchains
|
|
97
|
-
* merged is **not** resolved.
|
|
97
|
+
* merged is **not** resolved. 3.0.0 renames the file rather than accepting both:
|
|
98
98
|
* a deprecation window here would mean a repository could sit indefinitely on a
|
|
99
99
|
* name for a package that no longer exists, and the upgrade already requires
|
|
100
100
|
* touching the consumer's manifest and imports.
|
package/manifest.mjs
CHANGED
|
@@ -178,14 +178,23 @@ export function manifestPacks(config) {
|
|
|
178
178
|
pack,
|
|
179
179
|
...(pack.companions ?? []).flatMap(flatten),
|
|
180
180
|
];
|
|
181
|
-
return config.packs.flatMap(flatten).map((pack) =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
system
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
return config.packs.flatMap(flatten).map((pack) => {
|
|
182
|
+
// Foundry requires `system` on ActiveEffect, Actor and Item packs and
|
|
183
|
+
// on no others, so the value is per pack: its own declaration first,
|
|
184
|
+
// then the package-wide one. With neither, the key is omitted — an
|
|
185
|
+
// Adventure, Scene or JournalEntry pack that declares a system is
|
|
186
|
+
// hidden from every other system, which is rarely what a package that
|
|
187
|
+
// declined to name one meant.
|
|
188
|
+
const system = pack.system ?? config.stats.systemId;
|
|
189
|
+
return {
|
|
190
|
+
label: pack.label,
|
|
191
|
+
type: pack.type,
|
|
192
|
+
name: pack.name,
|
|
193
|
+
...(system ? { system } : {}),
|
|
194
|
+
path: `packs/${pack.name}`,
|
|
195
|
+
private: pack.private,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
package/types/config.d.mts
CHANGED
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
* @property {string|null} systemVersion That system's version, when the shared
|
|
48
48
|
* configuration stamps one.
|
|
49
49
|
* @property {string|null} containerImage Image override for every stage.
|
|
50
|
+
* @property {string|null} containerName Container name, minus the stage,
|
|
51
|
+
* shared with the packages that declare the same one so a single signed
|
|
52
|
+
* Foundry licence covers them all. `null` names it after the package.
|
|
50
53
|
* @property {Readonly<Record<string, Readonly<{port: number|null, world: string|null, version: string|null}>>>} containerStages
|
|
51
54
|
* Container stages beyond the conventional four.
|
|
52
55
|
* @property {string} e2eStage Which stage the suite runs against.
|
|
@@ -232,6 +235,12 @@ export type PackageBuildConfig = {
|
|
|
232
235
|
* Image override for every stage.
|
|
233
236
|
*/
|
|
234
237
|
containerImage: string | null;
|
|
238
|
+
/**
|
|
239
|
+
* Container name, minus the stage,
|
|
240
|
+
* shared with the packages that declare the same one so a single signed
|
|
241
|
+
* Foundry licence covers them all. `null` names it after the package.
|
|
242
|
+
*/
|
|
243
|
+
containerName: string | null;
|
|
235
244
|
/**
|
|
236
245
|
* Container stages beyond the conventional four.
|
|
237
246
|
*/
|
package/types/container.d.mts
CHANGED
|
@@ -11,15 +11,26 @@ export function dataEnvVar(stage: string): string;
|
|
|
11
11
|
/**
|
|
12
12
|
* The container name for a package's stage.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Stable, so Foundry's signed licence — which is bound to the container
|
|
15
|
+
* hostname — survives a recreate. Named after the package by default, so two
|
|
16
|
+
* HeroicLands packages run their own containers side by side.
|
|
17
|
+
*
|
|
18
|
+
* A declared `base` replaces the package-scoped half, which is what lets
|
|
19
|
+
* several packages share one container and so one signed licence: a signature
|
|
20
|
+
* is issued for a hostname, not for a package, and a package that cannot name
|
|
21
|
+
* the host cannot be pointed at a signature that already exists.
|
|
22
|
+
*
|
|
23
|
+
* The stage stays in the name either way. Two stages are two containers over
|
|
24
|
+
* two data roots, and docker names are unique: were a declared name used
|
|
25
|
+
* whole, `container dev` would find the `test` container already there, start
|
|
26
|
+
* it, and serve the test data root on the dev port.
|
|
17
27
|
*
|
|
18
28
|
* @param {string} packageId - The Foundry package id.
|
|
19
29
|
* @param {string} stage - The stage name.
|
|
30
|
+
* @param {string|null} [base] - Declared name, shareable between packages.
|
|
20
31
|
* @returns {string} The container name.
|
|
21
32
|
*/
|
|
22
|
-
export function containerName(packageId: string, stage: string): string;
|
|
33
|
+
export function containerName(packageId: string, stage: string, base?: string | null): string;
|
|
23
34
|
/**
|
|
24
35
|
* A stage declared in `packageBuild.container.stages`.
|
|
25
36
|
*
|
|
@@ -113,6 +113,20 @@ export type PackSpec = {
|
|
|
113
113
|
* folder documents are emitted.
|
|
114
114
|
*/
|
|
115
115
|
folders?: string | null | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Directory holding this pack's per-document
|
|
118
|
+
* JSON, already built. Declaring it skips
|
|
119
|
+
* generation for the pack and compiles from
|
|
120
|
+
* there instead.
|
|
121
|
+
*/
|
|
122
|
+
prebuilt?: string | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* The system this pack depends on, written
|
|
125
|
+
* to the manifest. Defaults to
|
|
126
|
+
* `stats.systemId`; omitted when neither is
|
|
127
|
+
* set.
|
|
128
|
+
*/
|
|
129
|
+
system?: string | undefined;
|
|
116
130
|
/**
|
|
117
131
|
* Packs written by this pack's own compiler
|
|
118
132
|
* pass rather than a pass of their own (the
|
|
@@ -148,6 +162,8 @@ export type ResolvedPackSpec = {
|
|
|
148
162
|
label: string;
|
|
149
163
|
private: boolean;
|
|
150
164
|
folders: string | null;
|
|
165
|
+
prebuilt: string | null;
|
|
166
|
+
system: string | null;
|
|
151
167
|
companions: readonly Readonly<ResolvedPackSpec>[];
|
|
152
168
|
mayBeEmpty: boolean;
|
|
153
169
|
default: boolean;
|
|
@@ -67,7 +67,7 @@ export const CONFIG_BASENAME: "package-build.config";
|
|
|
67
67
|
* reach for them: YAML first, `.mjs` last.
|
|
68
68
|
*
|
|
69
69
|
* The `content-build.config.*` stem this package read before the two toolchains
|
|
70
|
-
* merged is **not** resolved.
|
|
70
|
+
* merged is **not** resolved. 3.0.0 renames the file rather than accepting both:
|
|
71
71
|
* a deprecation window here would mean a repository could sit indefinitely on a
|
|
72
72
|
* name for a package that no longer exists, and the upgrade already requires
|
|
73
73
|
* touching the consumer's manifest and imports.
|