@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 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` 2.0.0
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 2.0.0;
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 2.0.0:
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": "^2.0.0"
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 2.0.0 the content half shipped as
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 2.0.0 these were two
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
- * Named after the package so two HeroicLands packages can run their own
114
- * containers side by side, and stable so Foundry's signed licence which is
115
- * bound to the container hostname survives a recreate.
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,
@@ -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
- systemId: requireNonEmptyString(input.systemId, "stats.systemId"),
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",
@@ -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
- const errors = await generatePacksJson({ only: packName, config });
98
- if (errors > 0) {
99
- throw new Error(
100
- `Pack JSON generation reported ${errors} error(s); refusing to ` +
101
- `compile packs from incomplete output.`,
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(`Pack ${name}: generated JSON not found at ${source}.`);
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
 
@@ -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. 2.0.0 renames the file rather than accepting both:
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
- 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
- }));
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.0.0",
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",
@@ -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
  */
@@ -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
- * Named after the package so two HeroicLands packages can run their own
15
- * containers side by side, and stable so Foundry's signed licence which is
16
- * bound to the container hostname survives a recreate.
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. 2.0.0 renames the file rather than accepting both:
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.