@heroiclands/package-build 0.3.0 → 0.5.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 +74 -0
- package/README.md +38 -5
- package/bin/package-build.mjs +82 -14
- package/bin/report.mjs +108 -0
- package/config.mjs +29 -0
- package/index.mjs +0 -3
- package/lang.mjs +2 -2
- package/package.json +1 -6
- package/types/config.d.mts +7 -0
- package/types/index.d.mts +0 -1
- package/text.mjs +0 -74
- package/types/text.d.mts +0 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 905254d: **`text.mjs` is deleted; the one implementation lives with the diagnostics
|
|
8
|
+
contract it serves.**
|
|
9
|
+
|
|
10
|
+
`locateInText` and `positionOf` computed which line and column a substring sits
|
|
11
|
+
on. `@heroiclands/content-build` computes the same thing in
|
|
12
|
+
`engine/diagnostics.mjs` as `positionOfLiteral`, and has said so in a comment
|
|
13
|
+
for some time:
|
|
14
|
+
|
|
15
|
+
> That is a duplicate worth naming: unlike the diagnostic _format_ or a
|
|
16
|
+
> validation _rule_, "which line and column is this substring on" has exactly
|
|
17
|
+
> one correct answer and cannot drift into disagreement. The tidier arrangement
|
|
18
|
+
> is for that package to re-export this one — the dependency runs that way — and
|
|
19
|
+
> it should, next time either is touched.
|
|
20
|
+
|
|
21
|
+
This is that time. Rather than re-export, the module is removed outright: the
|
|
22
|
+
`./text` subpath was a library surface with one internal caller (`lang.mjs`) and
|
|
23
|
+
one external one, and this package's job is a command line, not a text-utility
|
|
24
|
+
grab bag.
|
|
25
|
+
|
|
26
|
+
**Breaking for anyone importing `@heroiclands/package-build/text`.** Import
|
|
27
|
+
`positionOfLiteral` from `@heroiclands/content-build/engine/diagnostics`
|
|
28
|
+
instead; it is the same arithmetic, and returns `{}` rather than `undefined`
|
|
29
|
+
when the literal is absent — the shape the diagnostics contract already spreads.
|
|
30
|
+
`locateInText` had no callers anywhere and is simply gone.
|
|
31
|
+
|
|
32
|
+
## 0.4.0
|
|
33
|
+
|
|
34
|
+
### Minor Changes
|
|
35
|
+
|
|
36
|
+
- 2c2fc37: **`package-build bundle check` — the last capability that had no command.**
|
|
37
|
+
|
|
38
|
+
The bundle-loading check was exported as a library function and reachable no
|
|
39
|
+
other way, so a consumer that wanted it had to write the script the command line
|
|
40
|
+
exists to remove: read the manifest, read the bundle, call the function, decide
|
|
41
|
+
how to print findings, choose an exit code. It now runs from configuration like
|
|
42
|
+
every other job.
|
|
43
|
+
|
|
44
|
+
It catches three ways a package builds successfully and still does not load,
|
|
45
|
+
none of which a bundler can see, because each is a disagreement between two
|
|
46
|
+
files rather than a fault in either:
|
|
47
|
+
|
|
48
|
+
| The manifest says | What Foundry does |
|
|
49
|
+
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
50
|
+
| the entry under both `esmodules` and `scripts` | loads the bundle twice |
|
|
51
|
+
| the entry under neither | never loads it at all |
|
|
52
|
+
| the entry under `esmodules`, but the file only parses as a classic script | fails at load, naming whichever `import` came first and nothing about the manifest |
|
|
53
|
+
|
|
54
|
+
Both files are read from the stage, because the stage is what ships.
|
|
55
|
+
|
|
56
|
+
**The entry is derived, not stated.** `packageBuild.bundle.entry` defaults to
|
|
57
|
+
`<packageId>.mjs`, which is already derived from `package.json` `name`; a
|
|
58
|
+
repository states it only when its bundler emits something else. It is
|
|
59
|
+
deliberately _not_ read back out of the generated manifest — a value taken from
|
|
60
|
+
there would agree with itself by construction, and the check's whole question is
|
|
61
|
+
whether the manifest declares this file the way Foundry needs it.
|
|
62
|
+
|
|
63
|
+
**Reporting is now decided once.** `lang check` had the diagnostic contract —
|
|
64
|
+
`file:line:column: severity: message`, the path starting the line, a field
|
|
65
|
+
dropped rather than guessed — spelled out inline in its handler. Both commands
|
|
66
|
+
now report through one seam that maps findings onto the format
|
|
67
|
+
`@heroiclands/content-build` already owns, so the two packages cannot drift into
|
|
68
|
+
two nearly-identical formats. `lang check`'s output is unchanged.
|
|
69
|
+
|
|
70
|
+
**The command surface has a stated shape.** A capability with a single operation
|
|
71
|
+
is a bare command (`clean`, `assets`, `manifest`, `release`, `deploy <stage>`);
|
|
72
|
+
one with more than a single operation takes a positional action, so a second can
|
|
73
|
+
be added without renaming the first (`lang check`, `bundle check`).
|
|
74
|
+
|
|
75
|
+
Closes #12
|
|
76
|
+
|
|
3
77
|
## 0.3.0
|
|
4
78
|
|
|
5
79
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -96,6 +96,14 @@ packageBuild:
|
|
|
96
96
|
# Prefix of the shared SFTP override variables. Default `SOHL`.
|
|
97
97
|
envPrefix: SOHL
|
|
98
98
|
|
|
99
|
+
bundle:
|
|
100
|
+
# The file Foundry loads, as the manifest spells it, relative to the stage.
|
|
101
|
+
# Defaults to `<packageId>.mjs`; state it only when the bundler emits
|
|
102
|
+
# something else. Deliberately not read back out of the manifest — a value
|
|
103
|
+
# taken from there would agree with itself by construction, and `bundle
|
|
104
|
+
# check` asks whether the manifest declares this file correctly.
|
|
105
|
+
entry: sohl.mjs
|
|
106
|
+
|
|
99
107
|
# Optional. A module exporting `flags(config)` returning namespaced Foundry
|
|
100
108
|
# flags the repository has to *compute* — an address that only exists once
|
|
101
109
|
# the content tree has been walked, say. Merged over any declared below.
|
|
@@ -176,11 +184,12 @@ other's schema — they split by input, and the dependency runs one way.
|
|
|
176
184
|
|
|
177
185
|
**What is derived, not stated:**
|
|
178
186
|
|
|
179
|
-
| Field | Derived from
|
|
180
|
-
| -------------------------- |
|
|
181
|
-
| the repository root | the configuration file's own location
|
|
182
|
-
| `packageKind`, `packageId` | the shared configuration's top level
|
|
183
|
-
| the release artifact | `packageKind` — a system ships `system.json`, a module `module.json`
|
|
187
|
+
| Field | Derived from |
|
|
188
|
+
| -------------------------- | --------------------------------------------------------------------------- |
|
|
189
|
+
| the repository root | the configuration file's own location |
|
|
190
|
+
| `packageKind`, `packageId` | the shared configuration's top level |
|
|
191
|
+
| the release artifact | `packageKind` — a system ships `system.json`, a module `module.json` |
|
|
192
|
+
| the bundle entry | `packageId` — `<id>.mjs`, unless `packageBuild.bundle.entry` says otherwise |
|
|
184
193
|
|
|
185
194
|
## Command line
|
|
186
195
|
|
|
@@ -189,6 +198,7 @@ npx package-build clean [--distclean]
|
|
|
189
198
|
npx package-build assets
|
|
190
199
|
npx package-build manifest
|
|
191
200
|
npx package-build lang check
|
|
201
|
+
npx package-build bundle check
|
|
192
202
|
npx package-build release
|
|
193
203
|
npx package-build deploy <stage>
|
|
194
204
|
```
|
|
@@ -201,11 +211,34 @@ Wrapped as npm scripts — SoHL spells them:
|
|
|
201
211
|
"distclean": "package-build clean --distclean",
|
|
202
212
|
"build:assets": "package-build assets",
|
|
203
213
|
"lint:lang": "package-build lang check",
|
|
214
|
+
"lint:bundle-globals": "package-build bundle check",
|
|
204
215
|
"build:pack-release": "package-build release",
|
|
205
216
|
"push:qa": "package-build deploy qa"
|
|
206
217
|
}
|
|
207
218
|
```
|
|
208
219
|
|
|
220
|
+
**The shape of the surface.** A capability with a single operation is a bare
|
|
221
|
+
command (`clean`, `assets`, `manifest`, `release`, `deploy <stage>`); one with
|
|
222
|
+
more than a single operation takes a positional action, so a second can be added
|
|
223
|
+
without renaming the first (`lang check`, `bundle check`). Flat `lang:check`
|
|
224
|
+
names would make every operation a new top-level command and hide which ones
|
|
225
|
+
belong together.
|
|
226
|
+
|
|
227
|
+
**What `bundle check` checks.** Three ways a package builds successfully and
|
|
228
|
+
still does not load, none of which a bundler can see, because each is a
|
|
229
|
+
disagreement between two files rather than a fault in either:
|
|
230
|
+
|
|
231
|
+
| The manifest says | What Foundry does |
|
|
232
|
+
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
233
|
+
| the entry under both `esmodules` and `scripts` | loads the bundle twice |
|
|
234
|
+
| the entry under neither | never loads it at all |
|
|
235
|
+
| the entry under `esmodules`, but the file only parses as a classic script | fails at load, naming whichever `import` came first and nothing about the manifest |
|
|
236
|
+
|
|
237
|
+
It reads both files from the stage, because the stage is what ships — checking
|
|
238
|
+
sources would answer for a package nobody installs. Findings are emitted as
|
|
239
|
+
`file:line:column: severity: message`, and the command exits non-zero when any
|
|
240
|
+
of them is an error.
|
|
241
|
+
|
|
209
242
|
**Why the CLI exists.** This package was library-only, so every consuming
|
|
210
243
|
repository wrote a wrapper script per job — six of them in the SoHL repository,
|
|
211
244
|
441 lines that between them contained no logic. `clean.mjs` was 47 lines that
|
package/bin/package-build.mjs
CHANGED
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
* stay import-safe, so a consuming repository's build — or a test — can call
|
|
35
35
|
* them without any of it happening.
|
|
36
36
|
*
|
|
37
|
+
* **The shape of the command surface.** A capability with a single operation is
|
|
38
|
+
* a bare command (`clean`, `assets`, `manifest`, `release`, `deploy <stage>`);
|
|
39
|
+
* one with more than a single operation takes a positional action, so it can
|
|
40
|
+
* grow another without renaming the first (`lang check`, `bundle check`). The
|
|
41
|
+
* alternative — flat `lang:check`-style names — makes the second operation a
|
|
42
|
+
* new top-level command and the relationship between them invisible.
|
|
43
|
+
*
|
|
37
44
|
* The side effects that need *configuration* live inside the command handlers,
|
|
38
45
|
* never at module scope, so `--version` and `--help` answer in a directory with
|
|
39
46
|
* no configuration at all. Running an actual command still resolves it, and
|
|
@@ -44,6 +51,7 @@
|
|
|
44
51
|
* npx package-build assets
|
|
45
52
|
* npx package-build manifest
|
|
46
53
|
* npx package-build lang check
|
|
54
|
+
* npx package-build bundle check
|
|
47
55
|
* npx package-build release
|
|
48
56
|
* npx package-build deploy <stage>
|
|
49
57
|
*
|
|
@@ -51,6 +59,7 @@
|
|
|
51
59
|
* npm run clean // → … clean
|
|
52
60
|
* npm run build:assets // → … assets
|
|
53
61
|
* npm run lint:lang // → … lang check
|
|
62
|
+
* npm run lint:bundle-globals // → … bundle check
|
|
54
63
|
* npm run build:pack-release // → … release
|
|
55
64
|
* npm run push:qa // → … deploy qa
|
|
56
65
|
*/
|
|
@@ -66,9 +75,11 @@ import { loadPackageBuildConfig } from "../config.mjs";
|
|
|
66
75
|
import { loadPackConfig } from "@heroiclands/content-build/engine/pack-config";
|
|
67
76
|
import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
|
|
68
77
|
import { validateLangSource } from "../lang.mjs";
|
|
78
|
+
import { checkBundleLoading } from "../bundle.mjs";
|
|
69
79
|
import { packRelease } from "../release.mjs";
|
|
70
80
|
import { writeManifest } from "../manifest.mjs";
|
|
71
81
|
import { deployStage } from "../deploy.mjs";
|
|
82
|
+
import { reportFindings } from "./report.mjs";
|
|
72
83
|
|
|
73
84
|
/**
|
|
74
85
|
* This package's own version, for `--version`.
|
|
@@ -312,20 +323,10 @@ function langCommand() {
|
|
|
312
323
|
|
|
313
324
|
let total = 0;
|
|
314
325
|
for (const file of files.sort()) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
total++;
|
|
320
|
-
// The diagnostics contract: the path starts the line, and a
|
|
321
|
-
// field is dropped rather than guessed.
|
|
322
|
-
const at = [relative, finding.line, finding.column]
|
|
323
|
-
.filter((part) => part !== undefined && part !== null)
|
|
324
|
-
.join(":");
|
|
325
|
-
console.error(
|
|
326
|
-
`${at}: ${finding.severity ?? "error"}: ${finding.message}`,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
326
|
+
total += reportFindings(
|
|
327
|
+
validateLangSource(fs.readFileSync(file, "utf8")),
|
|
328
|
+
{ file: path.relative(config.rootDir, file) },
|
|
329
|
+
);
|
|
329
330
|
}
|
|
330
331
|
|
|
331
332
|
if (total) {
|
|
@@ -340,6 +341,72 @@ function langCommand() {
|
|
|
340
341
|
};
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
/**
|
|
345
|
+
* `bundle check` — does the manifest agree with the bundle it points at?
|
|
346
|
+
*
|
|
347
|
+
* Three ways a package can be built successfully and still not load, none of
|
|
348
|
+
* which the bundler can see because each is a disagreement between two files:
|
|
349
|
+
* the entry listed under both `esmodules` and `scripts` (Foundry loads it
|
|
350
|
+
* twice), under neither (Foundry never loads it), or under `esmodules` while
|
|
351
|
+
* the emitted file only parses as a classic script. The last fails at runtime
|
|
352
|
+
* with a message about whichever `import` came first and says nothing about the
|
|
353
|
+
* manifest, which is the kind of error that costs an afternoon.
|
|
354
|
+
*
|
|
355
|
+
* Both files are read from the stage, because the stage is what ships. Checking
|
|
356
|
+
* sources would answer for a package nobody installs.
|
|
357
|
+
*
|
|
358
|
+
* @returns {object} The yargs command module.
|
|
359
|
+
*/
|
|
360
|
+
function bundleCommand() {
|
|
361
|
+
return {
|
|
362
|
+
command: "bundle <action>",
|
|
363
|
+
describe: "Code-bundle checks",
|
|
364
|
+
builder: (y) =>
|
|
365
|
+
y.positional("action", {
|
|
366
|
+
choices: ["check"],
|
|
367
|
+
describe:
|
|
368
|
+
"check: verify the manifest and the staged bundle agree",
|
|
369
|
+
}),
|
|
370
|
+
handler: handler(() => {
|
|
371
|
+
const config = loadPackageBuildConfig();
|
|
372
|
+
const stageDir = path.join(config.rootDir, config.stageDir);
|
|
373
|
+
const manifestPath = path.join(stageDir, `${config.artifact}.json`);
|
|
374
|
+
const bundlePath = path.join(stageDir, config.bundleEntry);
|
|
375
|
+
|
|
376
|
+
// Named explicitly rather than left to a missing-file stack trace:
|
|
377
|
+
// both absences mean "the stage was not built", and a reader who
|
|
378
|
+
// sees the path knows which step to run.
|
|
379
|
+
for (const [what, where] of [
|
|
380
|
+
["manifest", manifestPath],
|
|
381
|
+
["bundle", bundlePath],
|
|
382
|
+
]) {
|
|
383
|
+
if (!fs.existsSync(where)) {
|
|
384
|
+
die(
|
|
385
|
+
`no staged ${what} at ` +
|
|
386
|
+
`${path.relative(config.rootDir, where)} — build ` +
|
|
387
|
+
`the stage before checking it.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const relative = path.relative(config.rootDir, bundlePath);
|
|
393
|
+
const { findings, declaredAs } = checkBundleLoading({
|
|
394
|
+
manifest: JSON.parse(fs.readFileSync(manifestPath, "utf8")),
|
|
395
|
+
source: fs.readFileSync(bundlePath, "utf8"),
|
|
396
|
+
entry: config.bundleEntry,
|
|
397
|
+
manifestName: path.relative(config.rootDir, manifestPath),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (reportFindings(findings, { file: relative })) process.exit(1);
|
|
401
|
+
|
|
402
|
+
console.log(
|
|
403
|
+
`package-build: ${config.bundleEntry} is declared under ` +
|
|
404
|
+
`"${declaredAs}" and loads as one.`,
|
|
405
|
+
);
|
|
406
|
+
}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
343
410
|
/**
|
|
344
411
|
* `release` — zip the staged package for a GitHub release.
|
|
345
412
|
*
|
|
@@ -421,6 +488,7 @@ yargs(hideBin(process.argv))
|
|
|
421
488
|
.command(assetsCommand())
|
|
422
489
|
.command(manifestCommand())
|
|
423
490
|
.command(langCommand())
|
|
491
|
+
.command(bundleCommand())
|
|
424
492
|
.command(releaseCommand())
|
|
425
493
|
.command(deployCommand())
|
|
426
494
|
.demandCommand(1, "Name a command.")
|
package/bin/report.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How the command line reports what a rule found.
|
|
16
|
+
*
|
|
17
|
+
* The package's rules are pure: they take source text and return findings,
|
|
18
|
+
* leaving discovery, I/O and reporting to whoever called them. That is what
|
|
19
|
+
* makes them testable, but it also means every caller has to decide the same
|
|
20
|
+
* two things — what a finding looks like once emitted, and what a run's exit
|
|
21
|
+
* code should be. Before there was a command line, every consumer decided them
|
|
22
|
+
* separately, and no two agreed.
|
|
23
|
+
*
|
|
24
|
+
* This module is where the binary decides them once. It lives under `bin/`
|
|
25
|
+
* rather than beside the rules deliberately: it is the *caller's* half, not
|
|
26
|
+
* part of the pure surface, and nothing importing this package as a library
|
|
27
|
+
* should reach it.
|
|
28
|
+
*
|
|
29
|
+
* The emitted form is the toolchain's diagnostic contract —
|
|
30
|
+
* `file:line:column: severity: message`, the path starting the line, a field
|
|
31
|
+
* dropped rather than guessed — which `@heroiclands/content-build` already
|
|
32
|
+
* owns. This module maps findings onto it rather than restating it, so the two
|
|
33
|
+
* packages cannot drift into two nearly-identical formats.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { emitDiagnostic } from "@heroiclands/content-build/engine/diagnostics";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A finding as the pure rules report one.
|
|
42
|
+
*
|
|
43
|
+
* @typedef {object} Finding
|
|
44
|
+
* @property {string} message What is wrong, in one sentence.
|
|
45
|
+
* @property {"warning"|"error"} [severity] Defaults to `error`.
|
|
46
|
+
* @property {number} [line] 1-based line, when known.
|
|
47
|
+
* @property {number} [column] 1-based column, when known.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Turn a rule's findings into diagnostics, attaching the file they are about.
|
|
52
|
+
*
|
|
53
|
+
* A rule is handed source text and never learns the path it came from, so the
|
|
54
|
+
* caller is the only one who can say it — and a finding without a file is one
|
|
55
|
+
* nothing can navigate to.
|
|
56
|
+
*
|
|
57
|
+
* **A field is dropped, never guessed.** A finding that knows no line emits as
|
|
58
|
+
* `file: severity: message`, because defaulting to `1:1` sends a reader to the
|
|
59
|
+
* top of the file every time and reads exactly like a real position. A column
|
|
60
|
+
* without a line is dropped for the same reason: it locates nothing on its own.
|
|
61
|
+
*
|
|
62
|
+
* @param {Finding[]} findings - What the rule returned.
|
|
63
|
+
* @param {object} opts
|
|
64
|
+
* @param {string} opts.file - Path to the file, relative to the working
|
|
65
|
+
* directory, so the emitted line is one an editor or `cc`-style parser can
|
|
66
|
+
* open.
|
|
67
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
68
|
+
* severity: "warning"|"error", message: string}>} The diagnostics, in the
|
|
69
|
+
* order the rule reported them.
|
|
70
|
+
*/
|
|
71
|
+
export function toDiagnostics(findings, { file }) {
|
|
72
|
+
return findings.map((finding) => {
|
|
73
|
+
const hasLine = finding.line !== undefined && finding.line !== null;
|
|
74
|
+
const hasColumn =
|
|
75
|
+
finding.column !== undefined && finding.column !== null;
|
|
76
|
+
return {
|
|
77
|
+
file,
|
|
78
|
+
...(hasLine ? { line: finding.line } : {}),
|
|
79
|
+
// Only alongside a line: `formatLocator` ignores a lone column, and
|
|
80
|
+
// carrying it anyway would invite a reader to trust it.
|
|
81
|
+
...(hasLine && hasColumn ? { column: finding.column } : {}),
|
|
82
|
+
severity: finding.severity ?? "error",
|
|
83
|
+
message: finding.message,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Emit a rule's findings and say how many of them fail the run.
|
|
90
|
+
*
|
|
91
|
+
* Warnings are emitted and counted separately from the return value, so a
|
|
92
|
+
* command can report something worth reading without failing a build over it.
|
|
93
|
+
*
|
|
94
|
+
* @param {Finding[]} findings - What the rule returned.
|
|
95
|
+
* @param {object} opts
|
|
96
|
+
* @param {string} opts.file - Path to the file, relative to the working dir.
|
|
97
|
+
* @param {(d: object) => void} [opts.emit] - How to emit one diagnostic.
|
|
98
|
+
* Injectable so a test can capture the emitted shape without reaching for
|
|
99
|
+
* the console; defaults to content-build's `emitDiagnostic`, which writes
|
|
100
|
+
* both severities to stderr.
|
|
101
|
+
* @returns {number} How many diagnostics were errors — the count a command
|
|
102
|
+
* turns into its exit code.
|
|
103
|
+
*/
|
|
104
|
+
export function reportFindings(findings, { file, emit = emitDiagnostic }) {
|
|
105
|
+
const diagnostics = toDiagnostics(findings, { file });
|
|
106
|
+
for (const diagnostic of diagnostics) emit(diagnostic);
|
|
107
|
+
return diagnostics.filter((d) => d.severity === "error").length;
|
|
108
|
+
}
|
package/config.mjs
CHANGED
|
@@ -69,6 +69,7 @@ const SECTION_KEYS = [
|
|
|
69
69
|
"lang",
|
|
70
70
|
"deploy",
|
|
71
71
|
"release",
|
|
72
|
+
"bundle",
|
|
72
73
|
];
|
|
73
74
|
|
|
74
75
|
/**
|
|
@@ -98,6 +99,7 @@ const CLEAN_KEYS = ["extra"];
|
|
|
98
99
|
const LANG_KEYS = ["sources", "help"];
|
|
99
100
|
const DEPLOY_KEYS = ["envPrefix"];
|
|
100
101
|
const RELEASE_KEYS = ["artifact"];
|
|
102
|
+
const BUNDLE_KEYS = ["entry"];
|
|
101
103
|
|
|
102
104
|
/**
|
|
103
105
|
* The artifact name each package kind ships, so no repository states it.
|
|
@@ -240,6 +242,8 @@ function normalizeManifest(value) {
|
|
|
240
242
|
* @property {string} langSources Glob for the localization files to check.
|
|
241
243
|
* @property {string|null} langHelp Extra guidance printed after a failure.
|
|
242
244
|
* @property {string} envPrefix Prefix of the deploy environment variables.
|
|
245
|
+
* @property {string} bundleEntry The bundle file Foundry loads, as the
|
|
246
|
+
* manifest spells it. Derived from the package id.
|
|
243
247
|
*/
|
|
244
248
|
|
|
245
249
|
/**
|
|
@@ -308,6 +312,15 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
308
312
|
);
|
|
309
313
|
const releaseInput = /** @type {Record<string, unknown>} */ (release);
|
|
310
314
|
|
|
315
|
+
const bundle = section.bundle ?? {};
|
|
316
|
+
if (!isMapping(bundle)) fail("packageBuild.bundle", "must be a mapping");
|
|
317
|
+
rejectUnknownKeys(
|
|
318
|
+
/** @type {Record<string, unknown>} */ (bundle),
|
|
319
|
+
BUNDLE_KEYS,
|
|
320
|
+
"packageBuild.bundle.",
|
|
321
|
+
);
|
|
322
|
+
const bundleInput = /** @type {Record<string, unknown>} */ (bundle);
|
|
323
|
+
|
|
311
324
|
return Object.freeze({
|
|
312
325
|
rootDir: shared.rootDir,
|
|
313
326
|
// Where the package is assembled before it is zipped or deployed. Every
|
|
@@ -375,6 +388,22 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
375
388
|
deployInput.envPrefix,
|
|
376
389
|
"packageBuild.deploy.envPrefix",
|
|
377
390
|
),
|
|
391
|
+
// The file Foundry loads, as the manifest spells it. Named after the
|
|
392
|
+
// package by convention, and the id is already derived from
|
|
393
|
+
// package.json `name` — so a repository states this only when its
|
|
394
|
+
// bundler emits something else.
|
|
395
|
+
//
|
|
396
|
+
// Deliberately *not* read back out of the generated manifest: the
|
|
397
|
+
// check's question is whether the manifest declares this file the way
|
|
398
|
+
// Foundry needs it, and a value taken from the manifest could never
|
|
399
|
+
// answer that — it would agree with itself by construction.
|
|
400
|
+
bundleEntry:
|
|
401
|
+
bundleInput.entry === undefined ?
|
|
402
|
+
`${shared.foundryPackage}.mjs`
|
|
403
|
+
: requireNonEmptyString(
|
|
404
|
+
bundleInput.entry,
|
|
405
|
+
"packageBuild.bundle.entry",
|
|
406
|
+
),
|
|
378
407
|
});
|
|
379
408
|
}
|
|
380
409
|
|
package/index.mjs
CHANGED
|
@@ -56,6 +56,3 @@ export * as deploy from "./deploy.mjs";
|
|
|
56
56
|
|
|
57
57
|
/** Localization files: what a shippable `lang/*.json` must satisfy. */
|
|
58
58
|
export * as lang from "./lang.mjs";
|
|
59
|
-
|
|
60
|
-
/** Locating a literal inside an arbitrary text file, for positioned findings. */
|
|
61
|
-
export * as text from "./text.mjs";
|
package/lang.mjs
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* @module
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
-
import {
|
|
48
|
+
import { positionOfLiteral } from "@heroiclands/content-build/engine/diagnostics";
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* A single finding, in the fields the shared diagnostic format takes.
|
|
@@ -140,7 +140,7 @@ export function validateLangSource(raw) {
|
|
|
140
140
|
* @param {string} key - The localization key.
|
|
141
141
|
* @returns {{line?: number, column?: number}} Spreadable position fields.
|
|
142
142
|
*/
|
|
143
|
-
const at = (key) =>
|
|
143
|
+
const at = (key) => positionOfLiteral(raw, `"${key}"`);
|
|
144
144
|
|
|
145
145
|
for (const [key, value] of Object.entries(json)) {
|
|
146
146
|
if (typeof value !== "string") continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -35,10 +35,6 @@
|
|
|
35
35
|
"types": "./types/stage.d.mts",
|
|
36
36
|
"import": "./stage.mjs"
|
|
37
37
|
},
|
|
38
|
-
"./text": {
|
|
39
|
-
"types": "./types/text.d.mts",
|
|
40
|
-
"import": "./text.mjs"
|
|
41
|
-
},
|
|
42
38
|
"./package.json": "./package.json",
|
|
43
39
|
"./config": {
|
|
44
40
|
"types": "./types/config.d.mts",
|
|
@@ -53,7 +49,6 @@
|
|
|
53
49
|
"manifest.mjs",
|
|
54
50
|
"release.mjs",
|
|
55
51
|
"stage.mjs",
|
|
56
|
-
"text.mjs",
|
|
57
52
|
"types",
|
|
58
53
|
"CHANGELOG.md",
|
|
59
54
|
"README.md",
|
package/types/config.d.mts
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* @property {string} langSources Glob for the localization files to check.
|
|
23
23
|
* @property {string|null} langHelp Extra guidance printed after a failure.
|
|
24
24
|
* @property {string} envPrefix Prefix of the deploy environment variables.
|
|
25
|
+
* @property {string} bundleEntry The bundle file Foundry loads, as the
|
|
26
|
+
* manifest spells it. Derived from the package id.
|
|
25
27
|
*/
|
|
26
28
|
/**
|
|
27
29
|
* Resolve a package-build configuration from an already-loaded shared one.
|
|
@@ -135,4 +137,9 @@ export type PackageBuildConfig = {
|
|
|
135
137
|
* Prefix of the deploy environment variables.
|
|
136
138
|
*/
|
|
137
139
|
envPrefix: string;
|
|
140
|
+
/**
|
|
141
|
+
* The bundle file Foundry loads, as the
|
|
142
|
+
* manifest spells it. Derived from the package id.
|
|
143
|
+
*/
|
|
144
|
+
bundleEntry: string;
|
|
138
145
|
};
|
package/types/index.d.mts
CHANGED
package/text.mjs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
-
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
-
*
|
|
5
|
-
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
-
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
-
*
|
|
8
|
-
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
-
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
-
*
|
|
11
|
-
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Locating a literal inside an arbitrary text file.
|
|
16
|
-
*
|
|
17
|
-
* A build check reports a **finding**, and a finding is only actionable if it
|
|
18
|
-
* says where it is (#1668). Most findings are *about* a string the check
|
|
19
|
-
* matched — a key, a marker, a caption — so its position is one string search
|
|
20
|
-
* away, and making that search is the difference between a finding that can be
|
|
21
|
-
* opened and one that has to be hunted for.
|
|
22
|
-
*
|
|
23
|
-
* `@heroiclands/content-build` owns the diagnostic **format**, and its
|
|
24
|
-
* `positionInBody` maps an offset within a parsed content note back to its
|
|
25
|
-
* file. That is a different job: the checks here read localization files,
|
|
26
|
-
* manifests, source and bundles — none of which are notes. So this module
|
|
27
|
-
* carries the generic operation, and nothing carries it twice.
|
|
28
|
-
*
|
|
29
|
-
* Plain ESM with no filesystem access, so it is unit-testable.
|
|
30
|
-
*
|
|
31
|
-
* @module
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Where a literal sits in a text.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} text - The file's contents.
|
|
38
|
-
* @param {string} needle - The literal to locate.
|
|
39
|
-
* @param {number} [occurrence] - Which occurrence, 1-based. Repeats of the same
|
|
40
|
-
* literal are otherwise indistinguishable, which is the symptom the
|
|
41
|
-
* diagnostic format exists to remove.
|
|
42
|
-
* @returns {{line: number, column: number}|undefined} 1-based position, or
|
|
43
|
-
* `undefined` when the literal is not there. A caller that gets `undefined`
|
|
44
|
-
* reports the file alone rather than a position that is not the problem.
|
|
45
|
-
*/
|
|
46
|
-
export function locateInText(text, needle, occurrence = 1) {
|
|
47
|
-
if (typeof text !== "string" || !needle) return undefined;
|
|
48
|
-
let at = -1;
|
|
49
|
-
for (let n = 0; n < occurrence; n++) {
|
|
50
|
-
at = text.indexOf(needle, at + 1);
|
|
51
|
-
if (at === -1) return undefined;
|
|
52
|
-
}
|
|
53
|
-
const before = text.slice(0, at);
|
|
54
|
-
return {
|
|
55
|
-
line: before.split("\n").length,
|
|
56
|
-
column: at - before.lastIndexOf("\n"),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Where a literal sits, as spreadable diagnostic fields.
|
|
62
|
-
*
|
|
63
|
-
* Keeps the drop-rather-than-guess rule in one place: an unfound literal
|
|
64
|
-
* contributes no position at all, rather than `undefined` fields that read as a
|
|
65
|
-
* bug or a `1:1` that sends the reader to the top of the file.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} text - The file's contents.
|
|
68
|
-
* @param {string} needle - The literal to locate.
|
|
69
|
-
* @param {number} [occurrence] - Which occurrence, 1-based.
|
|
70
|
-
* @returns {{line?: number, column?: number}} Spreadable position fields.
|
|
71
|
-
*/
|
|
72
|
-
export function positionOf(text, needle, occurrence = 1) {
|
|
73
|
-
return locateInText(text, needle, occurrence) ?? {};
|
|
74
|
-
}
|
package/types/text.d.mts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Locating a literal inside an arbitrary text file.
|
|
3
|
-
*
|
|
4
|
-
* A build check reports a **finding**, and a finding is only actionable if it
|
|
5
|
-
* says where it is (#1668). Most findings are *about* a string the check
|
|
6
|
-
* matched — a key, a marker, a caption — so its position is one string search
|
|
7
|
-
* away, and making that search is the difference between a finding that can be
|
|
8
|
-
* opened and one that has to be hunted for.
|
|
9
|
-
*
|
|
10
|
-
* `@heroiclands/content-build` owns the diagnostic **format**, and its
|
|
11
|
-
* `positionInBody` maps an offset within a parsed content note back to its
|
|
12
|
-
* file. That is a different job: the checks here read localization files,
|
|
13
|
-
* manifests, source and bundles — none of which are notes. So this module
|
|
14
|
-
* carries the generic operation, and nothing carries it twice.
|
|
15
|
-
*
|
|
16
|
-
* Plain ESM with no filesystem access, so it is unit-testable.
|
|
17
|
-
*
|
|
18
|
-
* @module
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* Where a literal sits in a text.
|
|
22
|
-
*
|
|
23
|
-
* @param {string} text - The file's contents.
|
|
24
|
-
* @param {string} needle - The literal to locate.
|
|
25
|
-
* @param {number} [occurrence] - Which occurrence, 1-based. Repeats of the same
|
|
26
|
-
* literal are otherwise indistinguishable, which is the symptom the
|
|
27
|
-
* diagnostic format exists to remove.
|
|
28
|
-
* @returns {{line: number, column: number}|undefined} 1-based position, or
|
|
29
|
-
* `undefined` when the literal is not there. A caller that gets `undefined`
|
|
30
|
-
* reports the file alone rather than a position that is not the problem.
|
|
31
|
-
*/
|
|
32
|
-
export function locateInText(text: string, needle: string, occurrence?: number): {
|
|
33
|
-
line: number;
|
|
34
|
-
column: number;
|
|
35
|
-
} | undefined;
|
|
36
|
-
/**
|
|
37
|
-
* Where a literal sits, as spreadable diagnostic fields.
|
|
38
|
-
*
|
|
39
|
-
* Keeps the drop-rather-than-guess rule in one place: an unfound literal
|
|
40
|
-
* contributes no position at all, rather than `undefined` fields that read as a
|
|
41
|
-
* bug or a `1:1` that sends the reader to the top of the file.
|
|
42
|
-
*
|
|
43
|
-
* @param {string} text - The file's contents.
|
|
44
|
-
* @param {string} needle - The literal to locate.
|
|
45
|
-
* @param {number} [occurrence] - Which occurrence, 1-based.
|
|
46
|
-
* @returns {{line?: number, column?: number}} Spreadable position fields.
|
|
47
|
-
*/
|
|
48
|
-
export function positionOf(text: string, needle: string, occurrence?: number): {
|
|
49
|
-
line?: number;
|
|
50
|
-
column?: number;
|
|
51
|
-
};
|