@heroiclands/package-build 0.4.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.
@@ -0,0 +1,359 @@
1
+ /**
2
+ * The environment variable naming a stage's Foundry data root.
3
+ *
4
+ * Derived rather than tabulated, so a repository that adds a stage of its own
5
+ * gets the variable without this package learning its name.
6
+ *
7
+ * @param {string} stage - The stage name.
8
+ * @returns {string} The variable to read.
9
+ */
10
+ export function dataEnvVar(stage: string): string;
11
+ /**
12
+ * The container name for a package's stage.
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.
17
+ *
18
+ * @param {string} packageId - The Foundry package id.
19
+ * @param {string} stage - The stage name.
20
+ * @returns {string} The container name.
21
+ */
22
+ export function containerName(packageId: string, stage: string): string;
23
+ /**
24
+ * A stage declared in `packageBuild.container.stages`.
25
+ *
26
+ * @typedef {object} ContainerStage
27
+ * @property {number|null} port Host port to publish.
28
+ * @property {string|null} world World to auto-launch; `""` forces none.
29
+ * @property {string|null} version Exact Foundry build to pin.
30
+ */
31
+ /**
32
+ * The host port a stage publishes.
33
+ *
34
+ * `FOUNDRYVTT_<STAGE>_PORT` first, then the stage's declared port, then the
35
+ * conventional default.
36
+ *
37
+ * @param {string} stage - The stage name.
38
+ * @param {object} [opts]
39
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
40
+ * @param {Record<string, ContainerStage>} [opts.stages] - Declared stages.
41
+ * @returns {number} The host port.
42
+ * @throws {Error} When the stage has no port from any source.
43
+ */
44
+ export function resolveStagePort(stage: string, { env, stages }?: {
45
+ env?: NodeJS.ProcessEnv | undefined;
46
+ stages?: Record<string, ContainerStage> | undefined;
47
+ }): number;
48
+ /**
49
+ * The exact Foundry build a stage is pinned to, or `null` to float.
50
+ *
51
+ * `FOUNDRYVTT_<STAGE>_VERSION` wins — that is how a sweep runs against a build
52
+ * without touching committed configuration. Then the stage's own declared
53
+ * version. Then, **for the end-to-end stage only**, the package's
54
+ * `compatibility.minimum`.
55
+ *
56
+ * That last rule is the point. `compatibility.minimum` is a promise the
57
+ * manifest makes to every user, and a promise is only defended if something
58
+ * exercises it: a regression that breaks the floor while working on a newer
59
+ * build passes a suite run above the floor in silence. Deriving the pin from
60
+ * the claim means the evidence and the claim are the same number, and neither
61
+ * can drift from the other.
62
+ *
63
+ * A floor that names no build (`"14"`) cannot pin one, so the run floats on the
64
+ * major tag — visibly, rather than by pretending to a precision it lacks.
65
+ *
66
+ * @param {string} stage - The stage name.
67
+ * @param {object} [opts]
68
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
69
+ * @param {Record<string, ContainerStage>} [opts.stages] - Declared stages.
70
+ * @param {string|null} [opts.compatibilityMinimum] - The claimed floor.
71
+ * @param {string} [opts.e2eStage] - Which stage the suite runs against.
72
+ * @returns {string|null} The exact build, or `null`.
73
+ */
74
+ export function resolveFoundryVersion(stage: string, { env, stages, compatibilityMinimum, e2eStage, }?: {
75
+ env?: NodeJS.ProcessEnv | undefined;
76
+ stages?: Record<string, ContainerStage> | undefined;
77
+ compatibilityMinimum?: string | null | undefined;
78
+ e2eStage?: string | undefined;
79
+ }): string | null;
80
+ /**
81
+ * The image a run uses.
82
+ *
83
+ * `FOUNDRYVTT_CONTAINER_IMAGE` wins, then `packageBuild.container.image`, then
84
+ * felddy's major tag for whichever version the package already implies — the
85
+ * pinned build, or failing that the compatibility floor. Only a package that
86
+ * claims neither floats.
87
+ *
88
+ * @param {object} [opts]
89
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
90
+ * @param {string|null} [opts.image] - The configured image.
91
+ * @param {string|null} [opts.version] - The pinned build, if any.
92
+ * @param {string|null} [opts.compatibilityMinimum] - The claimed floor.
93
+ * @returns {string} The image reference.
94
+ */
95
+ export function resolveImage({ env, image, version, compatibilityMinimum, }?: {
96
+ env?: NodeJS.ProcessEnv | undefined;
97
+ image?: string | null | undefined;
98
+ version?: string | null | undefined;
99
+ compatibilityMinimum?: string | null | undefined;
100
+ }): string;
101
+ /**
102
+ * The world a stage auto-launches.
103
+ *
104
+ * `null` leaves `FOUNDRY_WORLD` alone, so whatever the image was given decides.
105
+ * An empty string is a *declared* "never auto-launch" — the shape a stage whose
106
+ * world is managed by hand needs, and distinct from saying nothing.
107
+ *
108
+ * @param {string} stage - The stage name.
109
+ * @param {object} [opts]
110
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
111
+ * @param {Record<string, ContainerStage>} [opts.stages] - Declared stages.
112
+ * @returns {string|null} The world id, `""`, or `null`.
113
+ */
114
+ export function resolveWorld(stage: string, { env, stages }?: {
115
+ env?: NodeJS.ProcessEnv | undefined;
116
+ stages?: Record<string, ContainerStage> | undefined;
117
+ }): string | null;
118
+ /**
119
+ * A stage's dedicated Foundry licence key, if it has one.
120
+ *
121
+ * Foundry is single-seat, so running a `dev` and a `test` container at once
122
+ * needs two keys. `FOUNDRYVTT_<STAGE>_LICENSE_KEY` dedicates one to a stage,
123
+ * overriding any global `FOUNDRY_LICENSE_KEY` passed through.
124
+ *
125
+ * @param {string} stage - The stage name.
126
+ * @param {NodeJS.ProcessEnv} [env] - Environment to read.
127
+ * @returns {string|null} The key, or `null`.
128
+ */
129
+ export function resolveLicenseKey(stage: string, env?: NodeJS.ProcessEnv): string | null;
130
+ /**
131
+ * The Foundry data root for a stage, checked for what a bind mount needs.
132
+ *
133
+ * @param {string} stage - The stage name.
134
+ * @param {object} [opts]
135
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
136
+ * @returns {string} The local path.
137
+ * @throws {Error} When it is unset, or names a remote host.
138
+ */
139
+ export function resolveDataRoot(stage: string, { env }?: {
140
+ env?: NodeJS.ProcessEnv | undefined;
141
+ }): string;
142
+ /**
143
+ * The image's own environment variables, as key/value pairs.
144
+ *
145
+ * `CONTAINER_CACHE` is deliberately withheld: it names a path *inside* the
146
+ * container, and {@link dockerRunArgs} sets it to match a mount it actually
147
+ * makes. A host path from a `.env` file would otherwise reach the image naming
148
+ * a directory that is not there.
149
+ *
150
+ * @param {NodeJS.ProcessEnv} [env] - Environment to read.
151
+ * @returns {[string, string][]} The pairs to pass through.
152
+ */
153
+ export function passthroughEnv(env?: NodeJS.ProcessEnv): [string, string][];
154
+ /**
155
+ * The full `docker run` argument vector for a stage's container.
156
+ *
157
+ * Order matters at the end: docker takes the **last** `-e` for a repeated key,
158
+ * so the per-stage version, world and licence are appended after the
159
+ * passthrough and win over anything it carried.
160
+ *
161
+ * @param {object} opts
162
+ * @param {string} opts.name - Container name.
163
+ * @param {string} opts.image - Image reference.
164
+ * @param {number} opts.port - Host port to publish.
165
+ * @param {string} opts.dataRoot - Host directory to mount at `/data`.
166
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to pass through.
167
+ * @param {string|null} [opts.cacheDir] - Host download cache to mount.
168
+ * @param {string|null} [opts.version] - Exact build to pin.
169
+ * @param {string|null} [opts.world] - World to auto-launch; `""` forces none.
170
+ * @param {string|null} [opts.licenseKey] - Dedicated licence key.
171
+ * @returns {string[]} Arguments after `docker`.
172
+ */
173
+ export function dockerRunArgs({ name, image, port, dataRoot, env, cacheDir, version, world, licenseKey, }: {
174
+ name: string;
175
+ image: string;
176
+ port: number;
177
+ dataRoot: string;
178
+ env?: NodeJS.ProcessEnv | undefined;
179
+ cacheDir?: string | null | undefined;
180
+ version?: string | null | undefined;
181
+ world?: string | null | undefined;
182
+ licenseKey?: string | null | undefined;
183
+ }): string[];
184
+ /**
185
+ * Run the `docker` CLI, inheriting stdio.
186
+ *
187
+ * @param {string[]} args - Arguments after `docker`.
188
+ * @returns {number} The exit status.
189
+ * @throws {Error} When `docker` is not on `PATH`.
190
+ */
191
+ export function runDocker(args: string[]): number;
192
+ /**
193
+ * Run the `docker` CLI and capture its output, tolerating failure.
194
+ *
195
+ * @param {string[]} args - Arguments after `docker`.
196
+ * @returns {string} Trimmed stdout, or `""` when the command failed.
197
+ */
198
+ export function captureDocker(args: string[]): string;
199
+ /**
200
+ * @param {string} name - Container name.
201
+ * @param {boolean} [runningOnly] - Only count a running container.
202
+ * @returns {boolean} Whether a container with exactly this name is present.
203
+ */
204
+ export function containerExists(name: string, runningOnly?: boolean): boolean;
205
+ /**
206
+ * The HeroicLands-convention Foundry containers currently running.
207
+ *
208
+ * Used to warn about a single-seat licence clash before a run starts, which is
209
+ * why the filter is the shared `-foundry-` convention rather than one package's
210
+ * prefix: the clash is between *any* two licensed instances, not between two
211
+ * containers of the same package.
212
+ *
213
+ * @param {string} [except] - A container to omit from the list.
214
+ * @returns {string[]} The container names.
215
+ */
216
+ export function runningFoundryContainers(except?: string): string[];
217
+ /**
218
+ * Remove the data-root lock a container left behind when it did not shut down
219
+ * cleanly (`docker rm -f`, a crash, an OOM).
220
+ *
221
+ * Foundry then refuses to start with "already locked by another process",
222
+ * naming no owner — so a stale lock turns every later boot into a failure that
223
+ * reads like corruption rather than litter.
224
+ *
225
+ * **Only safe while the container is stopped**, which is exactly when every
226
+ * boot path here calls it: with nothing running against the data root, a lock
227
+ * present is by definition stale.
228
+ *
229
+ * @param {string} dataRoot - The Foundry user-data root.
230
+ * @param {(message: string) => void} [log] - Progress reporting.
231
+ */
232
+ export function clearStaleLock(dataRoot: string, log?: (message: string) => void): void;
233
+ /**
234
+ * Everything a container action needs, resolved from configuration and the
235
+ * environment once.
236
+ *
237
+ * @typedef {object} ResolvedContainer
238
+ * @property {string} stage
239
+ * @property {string} name
240
+ * @property {number} port
241
+ * @property {string} image
242
+ * @property {string} url
243
+ * @property {string|null} version
244
+ * @property {string|null} world
245
+ * @property {string|null} licenseKey
246
+ * @property {string|null} cacheDir
247
+ */
248
+ /**
249
+ * Resolve a stage's container settings.
250
+ *
251
+ * @param {object} opts
252
+ * @param {string} opts.stage - The stage name.
253
+ * @param {object} opts.config - The resolved package-build configuration.
254
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
255
+ * @returns {ResolvedContainer} The resolved settings.
256
+ */
257
+ export function resolveContainer({ stage, config, env }: {
258
+ stage: string;
259
+ config: object;
260
+ env?: NodeJS.ProcessEnv | undefined;
261
+ }): ResolvedContainer;
262
+ /**
263
+ * Start a stage's container, creating it when it does not exist yet.
264
+ *
265
+ * @param {ResolvedContainer} container - Resolved settings.
266
+ * @param {object} opts
267
+ * @param {string} opts.dataRoot - Host directory to mount at `/data`.
268
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to pass through.
269
+ * @param {(message: string) => void} [opts.log] - Progress reporting.
270
+ * @returns {number} The exit status.
271
+ */
272
+ export function startContainer(container: ResolvedContainer, { dataRoot, env, log }: {
273
+ dataRoot: string;
274
+ env?: NodeJS.ProcessEnv | undefined;
275
+ log?: ((message: string) => void) | undefined;
276
+ }): number;
277
+ /**
278
+ * Stop and remove a container, tolerating "not running" and "no such
279
+ * container".
280
+ *
281
+ * @param {string} name - Container name.
282
+ * @param {(message: string) => void} [log] - Progress reporting.
283
+ */
284
+ export function removeContainer(name: string, log?: (message: string) => void): void;
285
+ /**
286
+ * Perform one container action for a stage.
287
+ *
288
+ * @param {object} opts
289
+ * @param {string} opts.action - One of {@link CONTAINER_ACTIONS}.
290
+ * @param {string} opts.stage - The stage name.
291
+ * @param {object} opts.config - The resolved package-build configuration.
292
+ * @param {NodeJS.ProcessEnv} [opts.env] - Environment to read.
293
+ * @param {(message: string) => void} [opts.log] - Progress reporting.
294
+ * @returns {number} The exit status.
295
+ * @throws {Error} On an unknown action, or a stage with no usable data root.
296
+ */
297
+ export function containerAction({ action, stage, config, env, log, }: {
298
+ action: string;
299
+ stage: string;
300
+ config: object;
301
+ env?: NodeJS.ProcessEnv | undefined;
302
+ log?: ((message: string) => void) | undefined;
303
+ }): number;
304
+ /**
305
+ * Host port each conventional stage publishes, chosen so they can run at once.
306
+ *
307
+ * A stage a repository adds of its own declares its port in
308
+ * `packageBuild.container.stages`; these four need no entry because every
309
+ * HeroicLands package deploys to the same four.
310
+ */
311
+ export const DEFAULT_STAGE_PORTS: Readonly<{
312
+ dev: 30000;
313
+ qa: 30001;
314
+ prod: 30002;
315
+ test: 30003;
316
+ }>;
317
+ /** The port Foundry listens on inside the container. */
318
+ export const CONTAINER_PORT: 30000;
319
+ /**
320
+ * Where a host-provided download cache is mounted.
321
+ *
322
+ * A dedicated mount point rather than a subpath of `/data` keeps the cache
323
+ * independent of the data root, so one cache can serve every stage.
324
+ */
325
+ export const CACHE_MOUNT: "/container_cache";
326
+ /** What `package-build container` can be asked to do. */
327
+ export const CONTAINER_ACTIONS: readonly string[];
328
+ /**
329
+ * A stage declared in `packageBuild.container.stages`.
330
+ */
331
+ export type ContainerStage = {
332
+ /**
333
+ * Host port to publish.
334
+ */
335
+ port: number | null;
336
+ /**
337
+ * World to auto-launch; `""` forces none.
338
+ */
339
+ world: string | null;
340
+ /**
341
+ * Exact Foundry build to pin.
342
+ */
343
+ version: string | null;
344
+ };
345
+ /**
346
+ * Everything a container action needs, resolved from configuration and the
347
+ * environment once.
348
+ */
349
+ export type ResolvedContainer = {
350
+ stage: string;
351
+ name: string;
352
+ port: number;
353
+ image: string;
354
+ url: string;
355
+ version: string | null;
356
+ world: string | null;
357
+ licenseKey: string | null;
358
+ cacheDir: string | null;
359
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The roots a set of declared keys uses.
3
+ *
4
+ * Derived rather than configured by default, because a package's roots are
5
+ * already a fact about its localization file — asking for them again is one
6
+ * more thing to state and to get wrong. A repository states them only when it
7
+ * references a root the file does not yet declare at all.
8
+ *
9
+ * @param {Iterable<string>} keys - The declared keys.
10
+ * @returns {string[]} The distinct first segments, in name order.
11
+ */
12
+ export function keyRootsOf(keys: Iterable<string>): string[];
13
+ /**
14
+ * Read every localization reference out of a script.
15
+ *
16
+ * The **AST**, not the text, because a key named in a JSDoc `@example` is
17
+ * documentation: requiring it to exist would make the guard fail on prose, and
18
+ * counting it as a reference would let a comment keep a dead key alive.
19
+ *
20
+ * TypeScript's parser reads plain JavaScript too, so both go down one path
21
+ * rather than two that drift.
22
+ *
23
+ * @param {string} source - The file's contents.
24
+ * @param {object} options
25
+ * @param {string} options.file - Path to the file, for the findings.
26
+ * @param {readonly string[]} options.roots - The key roots.
27
+ * @returns {ReferenceSet} What the file references.
28
+ */
29
+ export function collectScriptReferences(source: string, { file, roots }: {
30
+ file: string;
31
+ roots: readonly string[];
32
+ }): ReferenceSet;
33
+ /**
34
+ * Read every localization reference out of a template.
35
+ *
36
+ * A text scan, because a template has no AST worth building for this: its keys
37
+ * sit in `{{localize "…"}}` calls and in helper hashes, and nothing in a `.hbs`
38
+ * file resembles a comment closely enough to mislead one.
39
+ *
40
+ * @param {string} source - The file's contents.
41
+ * @param {object} options
42
+ * @param {string} options.file - Path to the file, for the findings.
43
+ * @param {readonly string[]} options.roots - The key roots.
44
+ * @returns {ReferenceSet} What the template references.
45
+ */
46
+ export function collectTemplateReferences(source: string, { file, roots }: {
47
+ file: string;
48
+ roots: readonly string[];
49
+ }): ReferenceSet;
50
+ /**
51
+ * Combine reference sets into one.
52
+ *
53
+ * Keys are kept whole — every site that references a missing key is worth
54
+ * naming — while namespaces and patterns are de-duplicated, since neither says
55
+ * anything about where it came from.
56
+ *
57
+ * @param {Iterable<ReferenceSet>} sets - The sets to combine.
58
+ * @returns {ReferenceSet} One set holding all of them.
59
+ */
60
+ export function mergeReferences(sets: Iterable<ReferenceSet>): ReferenceSet;
61
+ /**
62
+ * Compare what a package declares against what it references.
63
+ *
64
+ * @param {object} options
65
+ * @param {string} options.langSource - The reference localization file's text.
66
+ * @param {string} options.langFile - Its path, for the findings about it.
67
+ * @param {ReferenceSet} options.references - Everything that references it.
68
+ * @param {readonly string[]} [options.retained] - Key prefixes to leave out of
69
+ * the advisory half. Each is a repository's statement that the keys under it
70
+ * are reached in a way no scan can see; the honest fix for an unreferenced
71
+ * key is still to delete it.
72
+ * @param {readonly string[]} [options.roots] - The key roots. Derived from the
73
+ * declared keys when absent.
74
+ * @returns {{findings: CoverageFinding[], unreferenced: CoverageFinding[],
75
+ * stats: object}} What must be fixed, what is merely worth reading, and what
76
+ * the run looked at. The two are separate because they are different
77
+ * questions: one says the package is broken, the other that it carries
78
+ * something nobody could see a use for.
79
+ */
80
+ export function analyzeCoverage({ langSource, langFile, references, retained, roots, }: {
81
+ langSource: string;
82
+ langFile: string;
83
+ references: ReferenceSet;
84
+ retained?: readonly string[] | undefined;
85
+ roots?: readonly string[] | undefined;
86
+ }): {
87
+ findings: CoverageFinding[];
88
+ unreferenced: CoverageFinding[];
89
+ stats: object;
90
+ };
91
+ /**
92
+ * One place a key is referenced, and how firmly.
93
+ */
94
+ export type KeyReference = {
95
+ /**
96
+ * - The localization key, in full.
97
+ */
98
+ key: string;
99
+ /**
100
+ * - Where it is referenced, relative to the repository
101
+ * root — the reference is the finding's site, not the localization file.
102
+ */
103
+ file: string;
104
+ /**
105
+ * - 1-based line, when it can be established.
106
+ */
107
+ line?: number | undefined;
108
+ /**
109
+ * - 1-based column, likewise.
110
+ */
111
+ column?: number | undefined;
112
+ /**
113
+ * - When true the key must be declared verbatim,
114
+ * even if it happens to be a prefix of keys that are. A *generated* key is
115
+ * minted whole, so keys sitting beneath it do not vouch for it; an ordinary
116
+ * textual reference to a family name does not have that property.
117
+ */
118
+ exact?: boolean | undefined;
119
+ /**
120
+ * - The verb phrase naming how the key is
121
+ * referenced, for the message: `references` by default, so a contributor of
122
+ * generated keys can say `defineType generates` instead.
123
+ */
124
+ origin?: string | undefined;
125
+ };
126
+ /**
127
+ * Everything one scan learned about how a file addresses localization.
128
+ */
129
+ export type ReferenceSet = {
130
+ /**
131
+ * - Concrete keys, each at its site.
132
+ */
133
+ keys: KeyReference[];
134
+ /**
135
+ * - Prefixes whose leaves are never named in
136
+ * source: a DataModel's `LOCALIZATION_PREFIXES`, the static head of a key
137
+ * built at runtime. A namespace vouches for *itself* being reachable, never
138
+ * for the keys beneath it.
139
+ */
140
+ namespaces: string[];
141
+ /**
142
+ * - Key shapes a dynamic construction can build,
143
+ * with `*` standing for one segment: `` `SOHL.Month.${i}.label` `` is
144
+ * `SOHL.Month.*.label`, and vouches for exactly what that expression can
145
+ * produce.
146
+ */
147
+ patterns: string[];
148
+ /**
149
+ * - What the scan could not resolve, in
150
+ * its own words.
151
+ */
152
+ findings: CoverageFinding[];
153
+ };
154
+ /**
155
+ * A finding about coverage.
156
+ *
157
+ * Unlike the rules in {@link module :lang}, these carry their own `file`: one
158
+ * run spans the localization file and every source that references it, so a
159
+ * single path supplied by the caller could not be right for all of them.
160
+ */
161
+ export type CoverageFinding = {
162
+ /**
163
+ * - Path the finding is about.
164
+ */
165
+ file: string;
166
+ /**
167
+ * - 1-based line, when known.
168
+ */
169
+ line?: number | undefined;
170
+ /**
171
+ * - 1-based column, when known.
172
+ */
173
+ column?: number | undefined;
174
+ /**
175
+ * - How it should be treated.
176
+ */
177
+ severity: "error" | "warning";
178
+ /**
179
+ * - What is wrong, in one sentence.
180
+ */
181
+ message: string;
182
+ };