@heroiclands/package-build 23.7.1 → 23.9.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 +41 -0
- package/README.md +32 -0
- package/bin/content-commands.mjs +32 -10
- package/ci/check-banners.mjs +37 -19
- package/container.mjs +48 -10
- package/docs/api.md +402 -58
- package/docs/authoring/first-note.md +1 -1
- package/docs/authoring/frontmatter.md +30 -1
- package/docs/authoring/links-and-markup.md +154 -56
- package/docs/commands.md +31 -12
- package/docs/content-format.md +25 -2
- package/docs/design.md +31 -0
- package/docs/diagnostics.md +1 -1
- package/docs/guides/e2e.md +37 -1
- package/docs/guides/packs.md +2 -1
- package/docs/guides/reference-pages.md +86 -63
- package/docs/project-setup.md +30 -15
- package/docs/reference/format-details.md +313 -132
- package/docs/reference/note-types.md +191 -174
- package/e2e.mjs +89 -2
- package/engine/anchors.mjs +7 -20
- package/engine/base-compiler.mjs +11 -18
- package/engine/compile-corpus.mjs +5 -3
- package/engine/content-alerts.mjs +4 -2
- package/engine/content-blocks.mjs +18 -36
- package/engine/content-format.yaml +1 -0
- package/engine/content-index.mjs +447 -33
- package/engine/content-links.mjs +43 -15
- package/engine/content-poetry.mjs +9 -0
- package/engine/content-tables.mjs +8 -4
- package/engine/culture-reach.mjs +61 -34
- package/engine/data-keys.mjs +3 -1
- package/engine/diagnostics.mjs +24 -11
- package/engine/dictionary-layout.mjs +151 -19
- package/engine/dictionary-rows.mjs +129 -79
- package/engine/foundry-entries.mjs +21 -3
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/generate.mjs +12 -22
- package/engine/glossary-entries.mjs +11 -11
- package/engine/heading-attributes.mjs +21 -28
- package/engine/helpers.mjs +20 -12
- package/engine/infobox.mjs +3 -0
- package/engine/journals.mjs +15 -2
- package/engine/language-data.mjs +53 -0
- package/engine/language-terms.mjs +86 -0
- package/engine/language-words.mjs +2 -2
- package/engine/lexical-sections.mjs +247 -0
- package/engine/markdownlint-run.mjs +12 -2
- package/engine/markup-validation.mjs +34 -0
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-claims.mjs +21 -10
- package/engine/note-package.mjs +3 -5
- package/engine/note-state.mjs +9 -3
- package/engine/note-vocabulary.mjs +36 -7
- package/engine/pdf-build.mjs +17 -10
- package/engine/prose-lint.mjs +12 -6
- package/engine/related-pages.mjs +22 -3
- package/engine/see-also.mjs +150 -0
- package/engine/site-build.mjs +21 -3
- package/engine/site-preview.mjs +4 -2
- package/engine/sql-tables.mjs +287 -53
- package/engine/standings.mjs +14 -12
- package/engine/stub-lint.mjs +6 -2
- package/engine/web-wikilinks.mjs +8 -6
- package/engine/word-rows.mjs +20 -12
- package/engine/word-terms.mjs +15 -6
- package/hugo-theme/layouts/partials/related.html +25 -8
- package/package.json +2 -1
- package/types/container.d.mts +4 -3
- package/types/e2e.d.mts +28 -0
- package/types/engine/base-compiler.d.mts +2 -2
- package/types/engine/compile-corpus.d.mts +2 -1
- package/types/engine/content-index.d.mts +85 -8
- package/types/engine/content-tables.d.mts +2 -1
- package/types/engine/culture-reach.d.mts +22 -2
- package/types/engine/data-keys.d.mts +7 -0
- package/types/engine/diagnostics.d.mts +7 -6
- package/types/engine/dictionary-layout.d.mts +35 -4
- package/types/engine/dictionary-rows.d.mts +9 -0
- package/types/engine/foundry-entries.d.mts +3 -1
- package/types/engine/heading-attributes.d.mts +2 -2
- package/types/engine/helpers.d.mts +11 -5
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/lexical-sections.d.mts +95 -0
- package/types/engine/markdownlint-run.d.mts +3 -1
- package/types/engine/markup-validation.d.mts +14 -0
- package/types/engine/note-addresses.d.mts +10 -0
- package/types/engine/note-claims.d.mts +7 -1
- package/types/engine/note-package.d.mts +3 -1
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +1 -0
- package/types/engine/related-pages.d.mts +10 -1
- package/types/engine/see-also.d.mts +55 -0
- package/types/engine/site-build.d.mts +4 -1
- package/types/engine/sql-tables.d.mts +40 -6
- package/types/engine/word-rows.d.mts +9 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
**Dictionaries and glossaries**
|
|
8
|
+
|
|
9
|
+
- Language dictionaries and declared culture or affiliation glossaries appear automatically in journals, sites and books; remove SQL fences for these defaults.
|
|
10
|
+
- Dictionaries use one full alphabetical list with field tags and one key; existing anchored Dictionary, Lexicon and Glossary headings keep their introductions and receive the lists.
|
|
11
|
+
- Language notes with nonempty `data.words`, and cultures or affiliations with nonempty `data.glossary`, need no prose body; glossary sections show only their owner's declared entries.
|
|
12
|
+
- Glossary entries naming dictionary words show their class and word link, with dictionary senses or the entry's context definition; sole-owner sense labels are omitted on that owner's page, while other attribution remains.
|
|
13
|
+
- Common marks belong only to glossary terms; select or filter `terms.common`, because queries using `dictionary.common` fail.
|
|
14
|
+
- Custom culture views select the culture's own entries and reached ranks or offices, applying common marks only to entries the culture owns.
|
|
15
|
+
- Glossary entries without definitions must name a word in their resolved language or a reachable rank or office; specify the correct language or supply a definition when a name cannot resolve.
|
|
16
|
+
|
|
17
|
+
**Links and publishing**
|
|
18
|
+
|
|
19
|
+
- Correct links to headings inside HTML comments or raw HTML, which publish no section; builds also refuse section links absent from a dependency's published notes, including pages without prose anchors or journal documents.
|
|
20
|
+
- Rebuild and fetch package indexes so dependent packages can link to generated sections and SQL-expression anchors; readable item documentation anchors target their documentation journals.
|
|
21
|
+
- Engine publishers with dynamic content must await `prepareContentIndex` and pass its publication to `emitContentIndex` before writing metadata.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Language dictionaries can show optional noun plurals and one verb pattern beside existing gender labels; languages without these details retain their output.
|
|
26
|
+
- Dictionary fragments without a key omit the verb table; key-only fragments print abbreviations alone.
|
|
27
|
+
- Built words show element senses in dictionary etymologies and pass the built-piece check, including declared variants, inherited elements and front-vowel affix forms.
|
|
28
|
+
- Public dictionaries omit GM-only citations, preventing false link errors from hidden notes.
|
|
29
|
+
|
|
30
|
+
## 23.8.0
|
|
31
|
+
|
|
32
|
+
### Minor Changes
|
|
33
|
+
|
|
34
|
+
Lint, Markdown checks and formatting now refuse malformed divs, alerts and poetry. Leave a blank line after a quote or alert before closing `:::`; formatting preserves malformed files for correction.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Authors can suggest related notes with optional explanations, shown in reading order on websites, in Foundry journals, and in books.
|
|
39
|
+
- End-to-end Cypress tests use the same pinned Chrome for Testing locally and in CI, with a reusable download cache and explicit browser overrides.
|
|
40
|
+
- Container tests wait for S3 to answer and explain how to recover safely when switching checkouts leaves an incompatible storage server.
|
|
41
|
+
- Content lint reports index, SQL and validation times in its final summary, making slow stages visible in build logs.
|
|
42
|
+
- Content lint and pack compilation spend less time repeating SQL queries and parsing notes, especially in large content packages.
|
|
43
|
+
|
|
3
44
|
## 23.7.1
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,6 +16,33 @@ npm install -D @heroiclands/package-build
|
|
|
16
16
|
|
|
17
17
|
The generated project includes a homepage, website and book targets, npm scripts, and a local `.gitignore`. [Getting started](docs/getting-started.md) routes you to a [system](docs/guides/system-project.md), [module](docs/guides/module-project.md), or [content authoring](docs/authoring/first-note.md) path. The [content-language-server](https://github.com/HeroicLands/content-language-server) provides editor navigation and completion.
|
|
18
18
|
|
|
19
|
+
## Working on this repository
|
|
20
|
+
|
|
21
|
+
Run `npm ci` in each fresh clone or worktree. Its `prepare` step installs the
|
|
22
|
+
committed `.githooks/` with `git config core.hooksPath .githooks`; run
|
|
23
|
+
`npm run prepare` to install the hooks again without reinstalling dependencies.
|
|
24
|
+
Git uses the hook files in the current worktree.
|
|
25
|
+
|
|
26
|
+
These are the canonical POSIX shell hooks from
|
|
27
|
+
[HeroicLands/.github](https://github.com/HeroicLands/.github#local-git-hooks-and-actionsgithooks).
|
|
28
|
+
They need Git, POSIX utilities and an available UTF-8 locale, and run without
|
|
29
|
+
Node at commit time. `pre-commit` refuses commits on `main` and `master`;
|
|
30
|
+
`commit-msg` refuses assistant coauthor credits and standalone assistant
|
|
31
|
+
signatures while allowing ordinary prose and human coauthors. Both forges run
|
|
32
|
+
the pinned canonical drift action against every installed hook file.
|
|
33
|
+
|
|
34
|
+
Local opt-outs use Git configuration: `hooks.protectedBranch false` or
|
|
35
|
+
`hooks.allowCommitOnMain true` for the branch guard, and
|
|
36
|
+
`hooks.noAttribution false` for the message guard. Prefix each with `git config`
|
|
37
|
+
to set it for this clone. `git commit --no-verify` skips both checks once.
|
|
38
|
+
Server and pull-request enforcement still apply.
|
|
39
|
+
|
|
40
|
+
The published `githooks/` bundle is a separate optional API described in
|
|
41
|
+
[project setup](docs/project-setup.md#optional-published-hook-bundle). This
|
|
42
|
+
repository’s setup installs the two canonical shell hooks; it installs no
|
|
43
|
+
pre-push hook. Run `npm test` and the other checks directly, or use
|
|
44
|
+
`package-build ci` for a container run.
|
|
45
|
+
|
|
19
46
|
## Documentation
|
|
20
47
|
|
|
21
48
|
The documentation lives in the Markdown files in this repository. Start with [the documentation home](docs/index.md). This toolchain does not build its own site or book. A Hugo theme dependency would form a cycle because the theme uses package-build for releases.
|
|
@@ -54,4 +81,9 @@ Every icon and image is covered by a provenance record stating `attribution`, `l
|
|
|
54
81
|
|
|
55
82
|
Leading captions, generic fenced divs, inline spans, and poetry code fences are described in [links and markup](docs/authoring/links-and-markup.md).
|
|
56
83
|
|
|
84
|
+
Cypress end-to-end suites use pinned Chrome for Testing 155.0.8059.84 by default
|
|
85
|
+
on macOS arm64 and Linux x64, downloaded once into a reusable cache. Select an
|
|
86
|
+
installed browser with `package-build e2e fast -- --browser electron` (also
|
|
87
|
+
available for `run` and `open`). See [browser selection and cache settings](docs/guides/e2e.md#choose-a-browser).
|
|
88
|
+
|
|
57
89
|
The package is GPL-3.0-or-later for code and CC-BY-SA-4.0 for original content. See [LICENSE.md](LICENSE.md).
|
package/bin/content-commands.mjs
CHANGED
|
@@ -101,17 +101,23 @@ import { HM3_ITEM_FIELDS } from "../hm3/item-fields.mjs";
|
|
|
101
101
|
import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
|
|
102
102
|
import { schemaSubtypeOf } from "../engine/subtype-registry.mjs";
|
|
103
103
|
import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
|
|
104
|
+
import { markupFindings } from "../engine/markup-validation.mjs";
|
|
104
105
|
import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
|
|
105
106
|
import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
|
|
106
107
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
107
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
emitContentIndex,
|
|
110
|
+
prepareContentIndex,
|
|
111
|
+
indexRecordsFor,
|
|
112
|
+
isAssetRecord,
|
|
113
|
+
} from "../engine/content-index.mjs";
|
|
108
114
|
import {
|
|
109
115
|
buildSite,
|
|
110
116
|
gatesFailed,
|
|
111
117
|
formatUnaddressableFinding as formatUnaddressable,
|
|
112
118
|
} from "../engine/site-build.mjs";
|
|
113
119
|
import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-links.mjs";
|
|
114
|
-
import { prepareTreeSqlTables } from "../engine/sql-tables.mjs";
|
|
120
|
+
import { prepareLinkSqlTables, prepareTreeSqlTables } from "../engine/sql-tables.mjs";
|
|
115
121
|
// The one place a link finding is worded, shared with both builds.
|
|
116
122
|
import { linkFindingMessage } from "../engine/wikilink-syntax.mjs";
|
|
117
123
|
import {
|
|
@@ -917,6 +923,7 @@ function lintCommand() {
|
|
|
917
923
|
// reader needs a line to open, not a stack.
|
|
918
924
|
const corpusProblems = [];
|
|
919
925
|
const parsed = new Map();
|
|
926
|
+
const indexStarted = performance.now();
|
|
920
927
|
const records = indexRecordsFor({
|
|
921
928
|
contentBase: root,
|
|
922
929
|
config,
|
|
@@ -924,12 +931,23 @@ function lintCommand() {
|
|
|
924
931
|
problems: corpusProblems,
|
|
925
932
|
parsed,
|
|
926
933
|
});
|
|
934
|
+
const indexMs = performance.now() - indexStarted;
|
|
927
935
|
for (const problem of corpusProblems) emitDiagnostic(problem);
|
|
928
936
|
// An error whatever the command's own strictness: the note is
|
|
929
937
|
// absent from every answer below, so reporting it and exiting 0
|
|
930
938
|
// would call the tree clean while silently omitting a note.
|
|
931
939
|
if (corpusProblems.length) process.exitCode = 1;
|
|
932
940
|
|
|
941
|
+
const sqlStarted = performance.now();
|
|
942
|
+
const sqlTables = await prepareTreeSqlTables(root, {
|
|
943
|
+
config,
|
|
944
|
+
records,
|
|
945
|
+
parsed,
|
|
946
|
+
skipDirectories: config.skipDirectories,
|
|
947
|
+
});
|
|
948
|
+
const sqlMs = performance.now() - sqlStarted;
|
|
949
|
+
const validationStarted = performance.now();
|
|
950
|
+
|
|
933
951
|
// The package is passed for the homepage rule, which
|
|
934
952
|
// names the address a tree with no front page fails to serve.
|
|
935
953
|
const addresses = lintContentTree(root, {
|
|
@@ -945,12 +963,7 @@ function lintCommand() {
|
|
|
945
963
|
config,
|
|
946
964
|
records,
|
|
947
965
|
skipDirectories: config.skipDirectories,
|
|
948
|
-
sqlTables
|
|
949
|
-
config,
|
|
950
|
-
records,
|
|
951
|
-
parsed,
|
|
952
|
-
skipDirectories: config.skipDirectories,
|
|
953
|
-
}),
|
|
966
|
+
sqlTables,
|
|
954
967
|
});
|
|
955
968
|
// Which system blocks this tree carries, derived from what it
|
|
956
969
|
// declares it ships for. Read before the lint so the
|
|
@@ -1182,6 +1195,7 @@ function lintCommand() {
|
|
|
1182
1195
|
});
|
|
1183
1196
|
|
|
1184
1197
|
const findings = [
|
|
1198
|
+
...index.notes.flatMap((note) => markupFindings(note.raw, note.file)),
|
|
1185
1199
|
...addresses.findings,
|
|
1186
1200
|
...frontmatter.findings,
|
|
1187
1201
|
...schemaFindings,
|
|
@@ -1198,6 +1212,7 @@ function lintCommand() {
|
|
|
1198
1212
|
// Only an **error** fails the run. `reportFindings` draws that
|
|
1199
1213
|
// line, so the rule is the shared one rather than a second
|
|
1200
1214
|
// copy here. Each finding names its own file.
|
|
1215
|
+
const validationMs = performance.now() - validationStarted;
|
|
1201
1216
|
const errors = reportFindings(findings, {});
|
|
1202
1217
|
if (errors) {
|
|
1203
1218
|
log.error(`${findings.length} finding(s) across ${addresses.notes} note(s).`);
|
|
@@ -1214,6 +1229,10 @@ function lintCommand() {
|
|
|
1214
1229
|
`${addresses.notes} note(s)).`,
|
|
1215
1230
|
);
|
|
1216
1231
|
}
|
|
1232
|
+
log.info(
|
|
1233
|
+
`Stage timings: index ${indexMs.toFixed(1)} ms; ` +
|
|
1234
|
+
`SQL ${sqlMs.toFixed(1)} ms; validation ${validationMs.toFixed(1)} ms.`,
|
|
1235
|
+
);
|
|
1217
1236
|
} catch (err) {
|
|
1218
1237
|
reportFailure(err);
|
|
1219
1238
|
process.exitCode = 1;
|
|
@@ -1424,7 +1443,7 @@ function linksCommand() {
|
|
|
1424
1443
|
config,
|
|
1425
1444
|
records,
|
|
1426
1445
|
...scope,
|
|
1427
|
-
sqlTables: await
|
|
1446
|
+
sqlTables: await prepareLinkSqlTables(contentBase, {
|
|
1428
1447
|
config,
|
|
1429
1448
|
records,
|
|
1430
1449
|
parsed,
|
|
@@ -1602,12 +1621,15 @@ function contentIndexCommand() {
|
|
|
1602
1621
|
type: "string",
|
|
1603
1622
|
});
|
|
1604
1623
|
},
|
|
1605
|
-
handler: (argv) => {
|
|
1624
|
+
handler: async (argv) => {
|
|
1606
1625
|
try {
|
|
1607
1626
|
const config = loadPackConfig();
|
|
1627
|
+
const contentBase = argv.root ?? config.paths.content;
|
|
1628
|
+
const publication = await prepareContentIndex({ config, contentBase });
|
|
1608
1629
|
const { file, notes, bytes, full, draft, stub, dictionary, terms } =
|
|
1609
1630
|
emitContentIndex({
|
|
1610
1631
|
config,
|
|
1632
|
+
publication,
|
|
1611
1633
|
...(argv.root ? { contentBase: argv.root } : {}),
|
|
1612
1634
|
...(argv.out ? { outDir: argv.out } : {}),
|
|
1613
1635
|
});
|
package/ci/check-banners.mjs
CHANGED
|
@@ -16,21 +16,16 @@
|
|
|
16
16
|
*
|
|
17
17
|
* `partials/hero-banner.html` needs to know which banner images exist, and Hugo
|
|
18
18
|
* cannot ask a remote asset host that question at build time — so the answer is
|
|
19
|
-
* declared in `data/banners.yaml` instead.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* names present in a local working copy of the artwork repository were not
|
|
23
|
-
* published, and three that were published appeared in no local copy at all.
|
|
24
|
-
*
|
|
25
|
-
* So this fetches every declared name and fails on any non-200. It is the test
|
|
26
|
-
* the template cannot make, moved to the one place that can make it.
|
|
19
|
+
* declared in `data/banners.yaml` instead. This checks every declaration and
|
|
20
|
+
* accepts HTTP 200 or 206. HTTP 404 and 410 identify absent artwork; other HTTP
|
|
21
|
+
* statuses and transport errors leave publication unverified and fail the check.
|
|
27
22
|
*
|
|
28
23
|
* It also refuses a name in `data/banner-kinds.yaml` that `banners.yaml` does
|
|
29
24
|
* not declare, before any request is made: a mapping to undrawn art is a dead
|
|
30
25
|
* URL on every page of that kind.
|
|
31
26
|
*
|
|
32
27
|
* Usage:
|
|
33
|
-
* node
|
|
28
|
+
* node ci/check-banners.mjs [--base <url>] [--concurrency <n>]
|
|
34
29
|
*
|
|
35
30
|
* The host defaults to the Heroic Lands CDN and can be pointed anywhere with
|
|
36
31
|
* `--base` or `BANNER_BASE_URL`, so a consumer publishing its own artwork set
|
|
@@ -158,6 +153,20 @@ function checkMapping(available) {
|
|
|
158
153
|
if (undeclared.length) process.exit(1);
|
|
159
154
|
}
|
|
160
155
|
|
|
156
|
+
/** Fetch wraps transport errors in a cause, which can hold several connection failures. */
|
|
157
|
+
function networkErrorDetails(error, seen = new Set()) {
|
|
158
|
+
if (!error || seen.has(error)) return [];
|
|
159
|
+
seen.add(error);
|
|
160
|
+
const message = [error.code, error.message].filter(Boolean).join(": ");
|
|
161
|
+
return [
|
|
162
|
+
...(message ? [message] : []),
|
|
163
|
+
...networkErrorDetails(error.cause, seen),
|
|
164
|
+
...(Array.isArray(error.errors) ?
|
|
165
|
+
error.errors.flatMap((cause) => networkErrorDetails(cause, seen))
|
|
166
|
+
: []),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
async function probe(url) {
|
|
162
171
|
try {
|
|
163
172
|
// HEAD first — it is what the question actually is. Some object stores
|
|
@@ -172,10 +181,15 @@ async function probe(url) {
|
|
|
172
181
|
}
|
|
173
182
|
return {
|
|
174
183
|
ok: res.status === 200 || res.status === 206,
|
|
175
|
-
|
|
184
|
+
missing: res.status === 404 || res.status === 410,
|
|
185
|
+
status: `HTTP ${res.status}`,
|
|
176
186
|
};
|
|
177
187
|
} catch (err) {
|
|
178
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
missing: false,
|
|
191
|
+
status: `network error: ${networkErrorDetails(err).join("; ") || String(err)}`,
|
|
192
|
+
};
|
|
179
193
|
}
|
|
180
194
|
}
|
|
181
195
|
|
|
@@ -190,26 +204,30 @@ async function main() {
|
|
|
190
204
|
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
191
205
|
for (let name = queue.shift(); name !== undefined; name = queue.shift()) {
|
|
192
206
|
const url = `${root}/${name}.webp`;
|
|
193
|
-
const { ok, status } = await probe(url);
|
|
194
|
-
if (!ok) failures.push({ name, url, status });
|
|
207
|
+
const { ok, missing, status } = await probe(url);
|
|
208
|
+
if (!ok) failures.push({ name, url, missing, status });
|
|
195
209
|
}
|
|
196
210
|
});
|
|
197
211
|
await Promise.all(workers);
|
|
198
212
|
|
|
199
213
|
if (failures.length) {
|
|
200
214
|
failures.sort((a, b) => a.name.localeCompare(b.name));
|
|
201
|
-
for (const { name, url, status } of failures) {
|
|
215
|
+
for (const { name, url, missing, status } of failures) {
|
|
202
216
|
diag(
|
|
203
217
|
lineOf(source, name),
|
|
204
|
-
`declared banner ${name} is not published (${status}): ${url}`,
|
|
218
|
+
`declared banner ${name} ${missing ? "is not published" : "could not be verified"} (${status}): ${url}`,
|
|
205
219
|
);
|
|
206
220
|
}
|
|
221
|
+
const missing = failures.filter((failure) => failure.missing).length;
|
|
222
|
+
const unverified = failures.length - missing;
|
|
207
223
|
console.error(
|
|
208
|
-
`check-banners: ${failures.length} of ${available.length} declared banners
|
|
209
|
-
);
|
|
210
|
-
console.error(
|
|
211
|
-
"Publish the artwork, or drop the name — an undrawn name here is a 404 on every page that resolves to it.",
|
|
224
|
+
`check-banners: ${failures.length} of ${available.length} declared banners failed verification at ${base} (${missing} missing, ${unverified} unverified).`,
|
|
212
225
|
);
|
|
226
|
+
if (missing) console.error("Publish the missing artwork or correct its inventory entry.");
|
|
227
|
+
if (unverified)
|
|
228
|
+
console.error(
|
|
229
|
+
"Check the asset host and request failures before changing the inventory.",
|
|
230
|
+
);
|
|
213
231
|
process.exit(1);
|
|
214
232
|
}
|
|
215
233
|
|
package/container.mjs
CHANGED
|
@@ -492,8 +492,8 @@ export function s3ShellArgs(plan, command) {
|
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
/**
|
|
495
|
-
* Start the S3 server on its network
|
|
496
|
-
*
|
|
495
|
+
* Start or reuse the S3 server on its network, wait for readiness, ensure the
|
|
496
|
+
* bucket exists, and copy the `awsConfig` into the data root.
|
|
497
497
|
*
|
|
498
498
|
* @param {object} opts
|
|
499
499
|
* @param {S3Plan} opts.plan - The S3 plan.
|
|
@@ -506,7 +506,8 @@ export function s3ShellArgs(plan, command) {
|
|
|
506
506
|
* @param {(ms: number) => void} [opts.sleep] - Waits.
|
|
507
507
|
* @param {number} [opts.attempts] - How often to ask the server whether it is
|
|
508
508
|
* ready.
|
|
509
|
-
* @throws {Error} When an input
|
|
509
|
+
* @throws {Error} When an input is not a readable regular file, a reused
|
|
510
|
+
* identities mount is incompatible, or the S3 HTTP service never answers.
|
|
510
511
|
*/
|
|
511
512
|
export function ensureS3({
|
|
512
513
|
plan,
|
|
@@ -521,6 +522,10 @@ export function ensureS3({
|
|
|
521
522
|
if (!fs.existsSync(file)) {
|
|
522
523
|
throw new Error(`FOUNDRYVTT_CONTAINER_S3 needs ${file}, which does not exist.`);
|
|
523
524
|
}
|
|
525
|
+
if (!fs.statSync(file).isFile()) {
|
|
526
|
+
throw new Error(`FOUNDRYVTT_CONTAINER_S3 needs ${file} to be a regular file.`);
|
|
527
|
+
}
|
|
528
|
+
fs.accessSync(file, fs.constants.R_OK);
|
|
524
529
|
}
|
|
525
530
|
const configDir = path.join(dataRoot, "Config");
|
|
526
531
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -540,6 +545,32 @@ export function ensureS3({
|
|
|
540
545
|
"{{.Names}}",
|
|
541
546
|
]);
|
|
542
547
|
if (present.split("\n").includes(plan.server)) {
|
|
548
|
+
let identitiesMount;
|
|
549
|
+
try {
|
|
550
|
+
const mounts = JSON.parse(
|
|
551
|
+
capture(["inspect", "--format", "{{json .Mounts}}", plan.server]),
|
|
552
|
+
);
|
|
553
|
+
identitiesMount = mounts.find(
|
|
554
|
+
(mount) => mount.Destination === "/etc/seaweedfs/s3.json",
|
|
555
|
+
);
|
|
556
|
+
} catch {
|
|
557
|
+
// An unreadable inspection cannot establish that this server is safe to reuse.
|
|
558
|
+
}
|
|
559
|
+
const source = identitiesMount?.Source;
|
|
560
|
+
const canonical = (file) =>
|
|
561
|
+
fs.existsSync(file) ? fs.realpathSync(file) : path.resolve(file);
|
|
562
|
+
if (
|
|
563
|
+
identitiesMount?.Type !== "bind" ||
|
|
564
|
+
typeof source !== "string" ||
|
|
565
|
+
canonical(source) !== canonical(plan.identities)
|
|
566
|
+
) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`S3 server '${plan.server}' has an incompatible identities mount ` +
|
|
569
|
+
`(found ${source ?? "no readable bind"}; expected ${plan.identities}). ` +
|
|
570
|
+
`Restore the original checkout, or remove only this disposable S3 container ` +
|
|
571
|
+
`with 'docker rm -f ${plan.server}' and rerun. Removing it discards its bucket data.`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
543
574
|
const running = capture([
|
|
544
575
|
"ps",
|
|
545
576
|
"--filter",
|
|
@@ -548,15 +579,22 @@ export function ensureS3({
|
|
|
548
579
|
"{{.Names}}",
|
|
549
580
|
]);
|
|
550
581
|
if (!running.split("\n").includes(plan.server)) run(["start", plan.server]);
|
|
551
|
-
|
|
582
|
+
} else {
|
|
583
|
+
log(`Starting S3 server '${plan.server}' (${S3_IMAGE}) as ${S3_ALIAS}:${S3_PORT}`);
|
|
584
|
+
const status = run(s3RunArgs(plan));
|
|
585
|
+
if (status !== 0) throw new Error(`Starting the S3 server failed with status ${status}.`);
|
|
552
586
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
587
|
+
// A ready filer does not mean the S3 listener has started. SeaweedFS's
|
|
588
|
+
// unauthenticated health route runs on that listener.
|
|
589
|
+
const probe = [
|
|
590
|
+
"exec",
|
|
591
|
+
plan.server,
|
|
592
|
+
"sh",
|
|
593
|
+
"-c",
|
|
594
|
+
`wget -q -T 2 -O /dev/null http://127.0.0.1:${S3_PORT}/healthz && echo ready`,
|
|
595
|
+
];
|
|
558
596
|
for (let attempt = 0; ; attempt++) {
|
|
559
|
-
if (capture(
|
|
597
|
+
if (capture(probe).trim() === "ready") break;
|
|
560
598
|
if (attempt >= attempts) {
|
|
561
599
|
throw new Error(`The S3 server '${plan.server}' did not become ready.`);
|
|
562
600
|
}
|