@heroiclands/package-build 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,91 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 53dd83b: **Commands for the Foundry container and the end-to-end harness** (#18).
8
+
9
+ A package that declares a `compatibility` range is making a promise, and until
10
+ now exactly one repository could defend it: container lifecycle, world seeding
11
+ and the browser harness all lived in `SoHL/utils/`, so two of three HeroicLands
12
+ module repositories declared a range nothing could test.
13
+
14
+ Two new commands:
15
+
16
+ ```
17
+ package-build container <stage> <start|stop|restart|recreate|rm|status|logs|pull>
18
+ package-build e2e <seed|run|open|fast|sweep>
19
+ ```
20
+
21
+ **Nothing about the destination is restated.** A container mounts
22
+ `FOUNDRYVTT_<STAGE>_DATA` — the variable `deploy` already writes into — so
23
+ serving what was just deployed is the next step from one variable rather than a
24
+ second configuration. Stage ports, the container name, the world id and the GM
25
+ credentials all derive from what the repository already declares.
26
+
27
+ **The end-to-end stage is pinned to `compatibility.minimum`.** The claim and the
28
+ evidence for it are now literally the same number, so raising the pin is what it
29
+ should be: a decision to raise the supported floor. `FOUNDRYVTT_<STAGE>_VERSION`
30
+ still wins for a one-off, and `e2e sweep <build>` runs the full suite against a
31
+ build the repository does _not_ pin, so `compatibility.verified` can be evidence.
32
+
33
+ **The wait is for an active world, not an open port.** Foundry answers on its
34
+ port long before a world is serving, and a suite started then fails every spec
35
+ for no visible reason. A licence failure — which never recovers — is read out of
36
+ the container log and reported at once.
37
+
38
+ **What the suite _is_ stays the consumer's**, named in `packageBuild.e2e.suite`
39
+ the way `assetTransform` and `manifestFlags` are named. So does the seed world's
40
+ extra content, in `packageBuild.e2e.documents`. A **module** package additionally
41
+ gets `core.moduleConfiguration` seeded, without which its suite would run against
42
+ a world that never loaded it.
43
+
44
+ New configuration, all optional: `packageBuild.container.{image,stages}` and
45
+ `packageBuild.e2e.{stage,suite,build,world,gm,documents}`. New subpath exports
46
+ `./container` and `./e2e`. Adds `@foundryvtt/foundryvtt-cli` as a dependency —
47
+ seeding a world means compiling its LevelDB collections.
48
+ - 3c631c3: Add the two localization guards that keep a package translated: `lang coverage`
49
+ and `lang hardcoded` (#19).
50
+
51
+ `lang check` already asked whether a localization file is _shippable_. Neither
52
+ of the questions that decide whether it is _translated_ was asked anywhere but
53
+ in the Song of Heroic Lands repository, in two scripts of its own — and both
54
+ satellites ship `lang/` files with no guard at all.
55
+
56
+ **`lang coverage`** — every key the package references exists, and every key it
57
+ declares is referenced. The two halves are not the same severity: a referenced
58
+ key that is missing renders to a player as its own raw key string and fails the
59
+ run; a declared key nothing references is reported and does not, because no scan
60
+ sees every way a key is reached and a guard that fails over one teaches people
61
+ to switch it off.
62
+
63
+ **`lang hardcoded`** — every user-visible literal in the templates goes through
64
+ localization, and every template still compiles once it does. This is the
65
+ _reverse_ walk, and it is the reason both exist: coverage walks key → file and
66
+ is blind to a template that names no key whatsoever. Before the work that
67
+ prompted this guard, SoHL had 516 hardcoded English literals across 61
68
+ templates, and translating every key in `en.json` would have left every one of
69
+ them in English.
70
+
71
+ **What a repository states.** Which files to scan is configuration and defaults
72
+ to the conventional layout, so a repository that follows it declares nothing.
73
+ The escape hatches — a key retained despite looking unreferenced, a literal
74
+ allowed despite looking like prose — each carry a required `reason`, because
75
+ each is a claim a reviewer has to be able to check.
76
+
77
+ **What stays the repository's.** `packageBuild.lang.references` names a module
78
+ exporting `references(context) -> ReferenceSet`, contributing the keys only that
79
+ repository's conventions can find — SoHL's `defineType(prefix, def)` mints one
80
+ key per member of an enum by a rule of its own. Everything Foundry-shaped is
81
+ built in: `{{localize}}`, `game.i18n.localize` / `format`, keys in string and
82
+ template literals, a DataModel's `LOCALIZATION_PREFIXES`, and the field
83
+ `label` / `hint` keys Foundry mints off one.
84
+
85
+ Scripts are read through the **AST**, so a key named in a JSDoc `@example` is
86
+ neither required to exist nor able to keep a dead key alive. `typescript` and
87
+ `handlebars` become runtime dependencies for that reason.
88
+
3
89
  ## 0.5.0
4
90
 
5
91
  ### Minor Changes
package/README.md CHANGED
@@ -41,6 +41,14 @@ The whole of assemble → validate → ship, one subpath each:
41
41
  - **`lang`** — what a shippable Foundry localization file must satisfy: it
42
42
  parses, its top level is an object, no key is both a leaf and a dotted prefix
43
43
  of another, placeholders are single-braced, and key segments carry no data.
44
+ - **`coverage`** — whether the keys a package references and the keys it
45
+ declares are the same set. A referenced key that is missing renders to a
46
+ player as its own raw key string, so it fails the run; a declared key nothing
47
+ references is reported and does not.
48
+ - **`templates`** — the opposite question, which coverage cannot ask: whether
49
+ the markup's user-visible text goes through localization at all, rather than
50
+ sitting in the file in English — and whether each template still compiles once
51
+ it does.
44
52
  - **`bundle`** — whether the manifest agrees with the file it points at.
45
53
  Declared under `"esmodules"` the bundle must parse as a module; declared under
46
54
  `"scripts"` it must declare **nothing** at top level, because every top-level
@@ -53,6 +61,12 @@ The whole of assemble → validate → ship, one subpath each:
53
61
  a local copy or SFTP. Always a staged, atomic swap: a running Foundry holds
54
62
  its LevelDB packs open, and replacing them in place leaves a directory LevelDB
55
63
  "repairs" to zero.
64
+ - **`container`** — running what was just deployed. The Foundry data root a
65
+ deploy writes into is the directory a container mounts, so serving the result
66
+ is the next step from one variable rather than a second configuration.
67
+ - **`e2e`** — a disposable world, seeded with a Gamemaster whose password is
68
+ known, served until it is genuinely **active**, and a suite driven against it.
69
+ What the suite _is_ stays the repository's.
56
70
  - **`text`** — locating a literal inside a file, so a finding names the line and
57
71
  column it is about.
58
72
 
@@ -89,9 +103,40 @@ packageBuild:
89
103
 
90
104
  lang:
91
105
  sources: lang/*.json
92
- # Printed after a failure — where this repository documents its key rules.
106
+ # Printed after a `lang check` failure — where this repository documents its
107
+ # key rules.
93
108
  help: See kb/dev-docs/reference/localization-keys.md.
94
109
 
110
+ # `lang coverage` and `lang hardcoded`. Every one of these defaults to the
111
+ # conventional layout, so a repository that follows it states none of them.
112
+ primary: lang/en.json # the file coverage is measured against
113
+ scripts: src/**/*.{ts,mjs} # scanned for key references
114
+ templates: templates/**/*.hbs # scanned for references and for English
115
+
116
+ # Only when the package references a root its file does not yet declare;
117
+ # otherwise the roots are read from the file's own keys.
118
+ keyRoots: [SOHL, TYPES, TYPE]
119
+
120
+ # Optional. A module exporting `references(context) -> ReferenceSet`,
121
+ # contributing the keys only this repository's conventions can find. SoHL's
122
+ # `defineType(prefix, def)` mints one key per member of an enum by a rule of
123
+ # its own; no shared guard can know it, and no repository can compare the
124
+ # result against the file. See "Contributing generated keys" below.
125
+ references: ./utils/lang-references.mjs
126
+
127
+ # Keys reached in a way no scan can see, exempt from the unreferenced
128
+ # advisory. The reason is required: it is a claim a reviewer has to be able
129
+ # to check, and the honest fix for an unreferenced key is to delete it.
130
+ retained:
131
+ - prefix: SOHL.Gear.Action.
132
+ reason: Titles built as `${titlePrefix}.${shortcode}` — the prefix is a parameter.
133
+
134
+ # Template literals that are deliberately not localization keys. The escape
135
+ # hatch, not the rule.
136
+ allow:
137
+ - literal: item.system.code === 'pyrn'
138
+ reason: An expression example shown as a placeholder — code, not prose.
139
+
95
140
  deploy:
96
141
  # Prefix of the shared SFTP override variables. Default `SOHL`.
97
142
  envPrefix: SOHL
@@ -135,6 +180,40 @@ packageBuild:
135
180
  socket: true
136
181
  grid: { distance: 5, units: ft }
137
182
  primaryTokenAttribute: health
183
+
184
+ # Running the deployed package in a Foundry container. The four conventional
185
+ # stages — dev, qa, prod, test — need no entry at all: their data root is
186
+ # `FOUNDRYVTT_<STAGE>_DATA`, the same variable `deploy` writes into, and their
187
+ # ports are conventional. Declare a stage only when it is genuinely yours.
188
+ container:
189
+ stages:
190
+ # SoHL keeps the previous, pre-TypeScript system on an older Foundry.
191
+ # An empty `world` declares "never auto-launch" — it is managed by hand.
192
+ leg: { port: 30000, world: "", version: "12.331" }
193
+
194
+ e2e:
195
+ # The one thing the harness does not own. Standing Foundry up and seeding a
196
+ # world is nobody's local problem; what runs against it is entirely yours.
197
+ suite:
198
+ run: [npx, cypress, run]
199
+ open: [npx, cypress, open]
200
+
201
+ # What the fast loop can rebuild, in the order it must be built: the
202
+ # bundler empties the stage, so it goes first. `recreate` marks a target
203
+ # whose output Foundry reads once, at world launch.
204
+ build:
205
+ code: build:code
206
+ assets: build:assets
207
+ db: build:db
208
+ system: { script: build:system, recreate: true }
209
+
210
+ # Everything here is optional — it derives from the package id otherwise.
211
+ world: { id: sohl-e2e, title: SoHL E2E }
212
+ gm: { name: Gamemaster, password: sohl-e2e }
213
+
214
+ # Extra world collections, compiled from directories of JSON documents.
215
+ documents:
216
+ actors: cypress/fixtures/actors
138
217
  ```
139
218
 
140
219
  ### The manifest is generated, not stamped
@@ -198,9 +277,13 @@ npx package-build clean [--distclean]
198
277
  npx package-build assets
199
278
  npx package-build manifest
200
279
  npx package-build lang check
280
+ npx package-build lang coverage [--unused]
281
+ npx package-build lang hardcoded
201
282
  npx package-build bundle check
202
283
  npx package-build release
203
284
  npx package-build deploy <stage>
285
+ npx package-build container <stage> <start|stop|restart|recreate|rm|status|logs|pull>
286
+ npx package-build e2e <seed|run|open|fast|sweep>
204
287
  ```
205
288
 
206
289
  Wrapped as npm scripts — SoHL spells them:
@@ -211,9 +294,15 @@ Wrapped as npm scripts — SoHL spells them:
211
294
  "distclean": "package-build clean --distclean",
212
295
  "build:assets": "package-build assets",
213
296
  "lint:lang": "package-build lang check",
297
+ "lint:lang-coverage": "package-build lang coverage",
298
+ "lint:lang-hardcoded": "package-build lang hardcoded",
214
299
  "lint:bundle-globals": "package-build bundle check",
215
300
  "build:pack-release": "package-build release",
216
- "push:qa": "package-build deploy qa"
301
+ "push:qa": "package-build deploy qa",
302
+ "container:dev": "package-build container dev",
303
+ "e2e:full": "package-build e2e run",
304
+ "e2e:fast": "package-build e2e fast",
305
+ "e2e:sweep": "package-build e2e sweep"
217
306
  }
218
307
  ```
219
308
 
@@ -224,6 +313,46 @@ without renaming the first (`lang check`, `bundle check`). Flat `lang:check`
224
313
  names would make every operation a new top-level command and hide which ones
225
314
  belong together.
226
315
 
316
+ **What the two localization guards ask.** They are deliberate opposites, and
317
+ neither can answer the other's question. `lang coverage` walks _key → file_, so
318
+ it is completely blind to a template that names no key whatsoever; `lang
319
+ hardcoded` walks _text → key_, and catches exactly that. Before the work that
320
+ prompted the second, Song of Heroic Lands had **516 hardcoded English literals
321
+ across 61 templates** — translating every key in `en.json` would have left every
322
+ one of them in English.
323
+
324
+ **Contributing generated keys.** `packageBuild.lang.references` names a module
325
+ exporting `references(context)`, called once with
326
+ `{ config, rootDir, roots, files }` — `files` being the scanned sources as
327
+ `{ path, text }`, already read, so the contributor sees exactly the text the
328
+ built-in scan saw. It returns a reference set:
329
+
330
+ ```js
331
+ export function references({ files }) {
332
+ return {
333
+ keys: [
334
+ // `exact` when the key is minted whole, so keys sitting beneath it do
335
+ // not vouch for it; `origin` is the verb phrase the message reads with.
336
+ {
337
+ key: "SOHL.Skill.CODE.lore",
338
+ file: "src/utils/constants.ts",
339
+ line: 42,
340
+ exact: true,
341
+ origin: "defineType generates",
342
+ },
343
+ ],
344
+ namespaces: ["SOHL.Skill.CODE"], // families whose leaves are never named
345
+ patterns: ["SOHL.Skill.*.label"], // shapes, `*` standing for one segment
346
+ findings: [], // whatever the contributor could not resolve
347
+ };
348
+ }
349
+ ```
350
+
351
+ Everything Foundry-shaped is already built in — `{{localize}}`,
352
+ `game.i18n.localize` / `format`, keys in string and template literals, a
353
+ DataModel's `LOCALIZATION_PREFIXES`, the `FIELDS.<field>.label` / `.hint` keys Foundry
354
+ mints off one. A contributor is for what only the repository knows.
355
+
227
356
  **What `bundle check` checks.** Three ways a package builds successfully and
228
357
  still does not load, none of which a bundler can see, because each is a
229
358
  disagreement between two files rather than a fault in either:
@@ -254,6 +383,113 @@ moved into configuration; the boilerplate lives in the CLI, once.
254
383
  `--version` and `--help` answer in a directory with no configuration at all.
255
384
  Running an actual command resolves it, and fails loudly when it is missing.
256
385
 
386
+ ## Standing Foundry up, and running a suite against it
387
+
388
+ A package that claims a `compatibility` range is making a promise, and a
389
+ promise is only defended if something exercises it. Until now exactly one
390
+ repository could: the container lifecycle, the world seeding and the browser
391
+ harness all lived in `SoHL/utils/`, so two of the three HeroicLands module
392
+ repositories declared a range nothing could test.
393
+
394
+ **The seam already existed.** `package-build deploy <stage>` installs a staged
395
+ package into `FOUNDRYVTT_<STAGE>_DATA`; a container mounts that same directory
396
+ at `/data` and serves it. Running Foundry against what was just deployed is the
397
+ next step from one variable, not a second configuration — so nothing about the
398
+ destination, the stage, or the package identity is restated.
399
+
400
+ ### Which Foundry build a run pins
401
+
402
+ `compatibility.minimum` is the oldest Foundry a package claims to run on, and
403
+ that claim is what the suite exists to defend. Testing above the floor tests a
404
+ configuration no user is promised while leaving the promised one unverified: a
405
+ regression that breaks the floor but works on a newer build passes in silence.
406
+
407
+ So **the end-to-end stage is pinned to `compatibility.minimum` itself**, read
408
+ from the top level of the shared configuration. The claim and the evidence are
409
+ the same number and cannot drift apart, and raising the pin is what it should
410
+ be — a decision to raise the supported floor, made by editing the claim.
411
+
412
+ `FOUNDRYVTT_<STAGE>_VERSION` still wins, so a contributor can sit on another
413
+ build without touching committed configuration. A floor that names no build
414
+ (`"14"`) cannot pin one; the run floats on the major tag, visibly, rather than
415
+ pretending to a precision it lacks.
416
+
417
+ ### The sweep
418
+
419
+ Routine runs go against the floor, which leaves the other direction untested: a
420
+ new Foundry release can break a package and nothing notices until a user does.
421
+
422
+ ```
423
+ npx package-build e2e sweep 14.367
424
+ ```
425
+
426
+ That is the full suite — reseeded world and all — against a build the
427
+ repository does not pin. It must be the full path rather than the fast one: a
428
+ seeded world is stamped with the build that created it, and Foundry refuses to
429
+ auto-launch a world stamped by another. It takes the build as an argument and
430
+ has **no default**, because the product of a sweep is a citable result ("the
431
+ full suite passed on 14.367") and "the newest release" is not a constant any
432
+ repository can hold without rotting.
433
+
434
+ A green sweep is what licenses moving `compatibility.verified` to that build. A
435
+ red one is the early warning the sweep exists to produce.
436
+
437
+ ### Waiting for a world, not for a port
438
+
439
+ Foundry answers on its port long before a world is serving. A suite started at
440
+ that moment fails every spec for no visible reason, which is the single most
441
+ expensive way an integration harness wastes an afternoon. So the wait is for
442
+ the join screen — the form a world renders only once it is **active** — and a
443
+ licence failure, which never recovers, is read out of the container log and
444
+ reported at once rather than after a three-minute timeout that explains
445
+ nothing.
446
+
447
+ ### The fast loop
448
+
449
+ ```
450
+ npx package-build e2e fast -- --spec cypress/e2e/skill.cy.js
451
+ ```
452
+
453
+ Rebuild what changed, redeploy, cycle the world, wait for it, re-run. It is one
454
+ command because every step of it has a quiet failure mode:
455
+
456
+ | Step | What goes wrong by hand |
457
+ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------- |
458
+ | Build | The container serves the **built** package, not the source tree. |
459
+ | Order | A bundler empties the stage, so building it after the asset and pack passes silently discards them — and the deploy is a mirror. |
460
+ | Manifest | Read once at world launch, so it needs a container **recreate**, not a restart. A target declaring `recreate: true` forces one. |
461
+ | Restart | A running Foundry holds its packs open, so a content change is invisible until the world reopens them. |
462
+ | Stale lock | A container that died holding the data-root lock makes every later boot fail with a message that names no owner. |
463
+ | Readiness | `docker start` returns long before Foundry serves. |
464
+ | The suite | Editor and agent shells export `ELECTRON_RUN_AS_NODE`, which makes an Electron runner start as plain Node and die on its own flags. |
465
+
466
+ `--build` takes a comma-separated list of declared targets, `all`, or `none`;
467
+ an unrecognised one fails fast rather than half-deploying. `--recreate` forces
468
+ a recreate, `--no-run` stops once the environment is current, and everything
469
+ else — including anything after a bare `--` — is handed to the suite verbatim.
470
+
471
+ ### What the seeded world holds
472
+
473
+ A world Foundry will launch without a migration prompt: `world.json`, and one
474
+ Gamemaster whose password is known, so a spec can log in deterministically. It
475
+ also carries one **active** scene, because an empty world auto-starts Foundry's
476
+ welcome tour (whose callout overlays sheets) and a world with no active scene
477
+ has no ready canvas.
478
+
479
+ For a **module** package it additionally writes `core.moduleConfiguration`,
480
+ switching the module on. A system is the world's own and `world.json` names it;
481
+ a module is not, and without that setting a module repository would stand its
482
+ suite up against a world that never loaded the thing under test.
483
+
484
+ Anything else — which actors, which journals — is the repository's, declared as
485
+ `packageBuild.e2e.documents` and compiled in beside the built-ins.
486
+
487
+ The world directory is wiped and rewritten on every seed. That is what makes a
488
+ run repeatable, and it is why `FOUNDRYVTT_<E2E_STAGE>_DATA` must be a separate,
489
+ empty directory: pointing it at a working stage would let the seed delete
490
+ worlds there, and would make the image reuse that stage's `Config/license.json`
491
+ in place of the key dedicated to the suite.
492
+
257
493
  ## Design
258
494
 
259
495
  **The rules are pure, and I/O is confined to functions named for it.** A rule