@heroiclands/package-build 23.12.3 → 23.12.5
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 +28 -0
- package/ci/ci-steps.mjs +5 -5
- package/content-config.mjs +19 -0
- package/docs/api.md +1 -0
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +4 -4
- package/docs/commands.md +5 -4
- package/docs/configuration.md +29 -12
- package/docs/guides/book.md +15 -13
- package/docs/guides/release-and-deploy.md +6 -9
- package/docs/project-setup.md +11 -11
- package/docs/reference/format-details.md +28 -16
- package/docs/reference/note-types.md +1 -1
- package/engine/content-index.mjs +2 -39
- package/engine/event-fields.mjs +24 -1
- package/engine/event-rows.mjs +37 -2
- package/engine/event-views.mjs +58 -14
- package/engine/frontmatter-lint.mjs +11 -0
- package/engine/generated-sections.mjs +8 -1
- package/engine/holdings.mjs +8 -2
- package/engine/infobox-render.mjs +16 -5
- package/engine/infobox.mjs +23 -2
- package/engine/literature-works.mjs +8 -2
- package/engine/metadata-index.mjs +3 -0
- package/engine/name-sort.mjs +91 -0
- package/engine/note-frontmatter.mjs +1 -1
- package/engine/note-vocabulary.mjs +1 -1
- package/engine/page-lists.mjs +3 -2
- package/engine/pdf-build.mjs +1 -0
- package/engine/pdf-index.mjs +33 -12
- package/engine/pdf-pages.mjs +13 -3
- package/engine/pdf-render.mjs +60 -33
- package/engine/pdf-toc.mjs +4 -3
- package/engine/project-init.mjs +2 -2
- package/engine/related-pages.mjs +12 -2
- package/engine/site-build.mjs +3 -0
- package/engine/sql-tables.mjs +20 -5
- package/githooks/pre-push +5 -5
- package/package.json +1 -1
- package/types/content-config.d.mts +5 -0
- package/types/engine/content-index.d.mts +2 -31
- package/types/engine/event-fields.d.mts +11 -0
- package/types/engine/event-rows.d.mts +4 -1
- package/types/engine/event-views.d.mts +7 -3
- package/types/engine/generated-sections.d.mts +6 -0
- package/types/engine/holdings.d.mts +7 -1
- package/types/engine/infobox.d.mts +11 -0
- package/types/engine/literature-works.d.mts +5 -0
- package/types/engine/name-sort.d.mts +49 -0
- package/types/engine/pdf-index.d.mts +7 -3
- package/types/engine/pdf-pages.d.mts +2 -1
- package/types/engine/pdf-render.d.mts +16 -5
- package/types/engine/pdf-toc.d.mts +1 -1
- package/types/engine/related-pages.d.mts +5 -0
- package/ci/confirm-npm-version.mjs +0 -135
|
@@ -34,18 +34,22 @@ export function eventViewSections(db: object, records: Iterable<object>, fileOf:
|
|
|
34
34
|
*
|
|
35
35
|
* @type {ReadonlyArray<Readonly<{slug: string, heading: string,
|
|
36
36
|
* applies: (record: object) => boolean,
|
|
37
|
-
* query: (self: string[], shape: CorpusShape) => {fence: string, own?: string}|null,
|
|
38
|
-
* owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string
|
|
37
|
+
* query: (self: string[], shape: CorpusShape, columns?: {felt?: boolean}) => {fence: string, own?: string}|null,
|
|
38
|
+
* owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>,
|
|
39
|
+
* felt?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>}>>}
|
|
39
40
|
*/
|
|
40
41
|
export const EVENT_VIEWS: ReadonlyArray<Readonly<{
|
|
41
42
|
slug: string;
|
|
42
43
|
heading: string;
|
|
43
44
|
applies: (record: object) => boolean;
|
|
44
|
-
query: (self: string[], shape: CorpusShape
|
|
45
|
+
query: (self: string[], shape: CorpusShape, columns?: {
|
|
46
|
+
felt?: boolean;
|
|
47
|
+
}) => {
|
|
45
48
|
fence: string;
|
|
46
49
|
own?: string;
|
|
47
50
|
} | null;
|
|
48
51
|
owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>;
|
|
52
|
+
felt?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>;
|
|
49
53
|
}>>;
|
|
50
54
|
export type CorpusShape = {
|
|
51
55
|
/**
|
|
@@ -110,6 +110,12 @@ export function splitGenerated(markdown: string): {
|
|
|
110
110
|
*
|
|
111
111
|
* @module
|
|
112
112
|
*/
|
|
113
|
+
/**
|
|
114
|
+
* The anchor of the Chronology section, which the book sets across the page.
|
|
115
|
+
*
|
|
116
|
+
* @type {string}
|
|
117
|
+
*/
|
|
118
|
+
export const CHRONOLOGY_SLUG: string;
|
|
113
119
|
/**
|
|
114
120
|
* Every generated section, in the order it follows the author's text: where a
|
|
115
121
|
* place sits and who governs it, then what happened, then what was written
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/** @typedef {{title:string,url?:string,address:string,type:string,subType?:string}} HoldingsEntry */
|
|
1
|
+
/** @typedef {{title:string,sort?:string,url?:string,address:string,type:string,subType?:string}} HoldingsEntry */
|
|
2
2
|
/**
|
|
3
3
|
* @typedef {object} HoldingsNode
|
|
4
4
|
* @property {string} shortcode
|
|
5
5
|
* @property {"place"|"affiliation"} type
|
|
6
6
|
* @property {string} [subType]
|
|
7
7
|
* @property {string} title
|
|
8
|
+
* @property {string} [sort] The note's `name.sort`; the list files by it in place of `title`.
|
|
8
9
|
* @property {string} [url]
|
|
9
10
|
* @property {string} [package] Owning content package.
|
|
10
11
|
* @property {string} [system] Own document system.
|
|
@@ -60,6 +61,7 @@ export function holdingsPages(nodes: Iterable<HoldingsNode | null>, { government
|
|
|
60
61
|
export function checkGovernment(note: object): object[];
|
|
61
62
|
export type HoldingsEntry = {
|
|
62
63
|
title: string;
|
|
64
|
+
sort?: string;
|
|
63
65
|
url?: string;
|
|
64
66
|
address: string;
|
|
65
67
|
type: string;
|
|
@@ -70,6 +72,10 @@ export type HoldingsNode = {
|
|
|
70
72
|
type: "place" | "affiliation";
|
|
71
73
|
subType?: string | undefined;
|
|
72
74
|
title: string;
|
|
75
|
+
/**
|
|
76
|
+
* The note's `name.sort`; the list files by it in place of `title`.
|
|
77
|
+
*/
|
|
78
|
+
sort?: string | undefined;
|
|
73
79
|
url?: string | undefined;
|
|
74
80
|
/**
|
|
75
81
|
* Owning content package.
|
|
@@ -94,6 +94,17 @@ export function hasRenderableValue(kind: string, value: unknown): boolean;
|
|
|
94
94
|
* @returns {string} One of {@link INFOBOX_VALUE_KINDS}.
|
|
95
95
|
*/
|
|
96
96
|
export function valueKindOf(field: object, value: unknown): string;
|
|
97
|
+
/**
|
|
98
|
+
* A number as a reader reads it in a profile: under a million in full with
|
|
99
|
+
* thousands separators (`850`, `12,500`), and from a million up in words with
|
|
100
|
+
* at most one decimal (`11 million`, `1.2 million`, `3 billion`). Every figure
|
|
101
|
+
* the setting gives at that scale is an estimate, and the words say so.
|
|
102
|
+
* Decimals below a million keep the digits they were written with.
|
|
103
|
+
*
|
|
104
|
+
* @param {number} value - The number.
|
|
105
|
+
* @returns {string} The text the row shows.
|
|
106
|
+
*/
|
|
107
|
+
export function formatQuantity(value: number): string;
|
|
97
108
|
/**
|
|
98
109
|
* One row, with its unit on it.
|
|
99
110
|
*
|
|
@@ -38,6 +38,7 @@ export function worksPages(nodes: Iterable<WorksNode | null>, { types }: {
|
|
|
38
38
|
}>;
|
|
39
39
|
export type WorkEntry = {
|
|
40
40
|
title: string;
|
|
41
|
+
sort?: string;
|
|
41
42
|
url?: string;
|
|
42
43
|
address: string;
|
|
43
44
|
form?: string;
|
|
@@ -48,6 +49,10 @@ export type WorksNode = {
|
|
|
48
49
|
*/
|
|
49
50
|
key: string;
|
|
50
51
|
title: string;
|
|
52
|
+
/**
|
|
53
|
+
* The note's `name.sort`; the list files by it in place of `title`.
|
|
54
|
+
*/
|
|
55
|
+
sort?: string | undefined;
|
|
51
56
|
/**
|
|
52
57
|
* Absent for a note that publishes no page.
|
|
53
58
|
*/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A note's display name reduced to printable 7-bit ASCII.
|
|
3
|
+
*
|
|
4
|
+
* Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
|
|
5
|
+
* `Kèthîra` — and nobody types them. A reader searching the index, or an editor
|
|
6
|
+
* completing a wikilink, needs a form that matches what a keyboard produces, so
|
|
7
|
+
* the record states one rather than leaving every consumer to invent it (and to
|
|
8
|
+
* invent a *different* one, which is how two searches over the same data come
|
|
9
|
+
* to disagree).
|
|
10
|
+
*
|
|
11
|
+
* **Transliterated, not stripped.** `unidecode` — the same table
|
|
12
|
+
* {@link slugify} already runs, so an ASCII name and a slug can never disagree
|
|
13
|
+
* about a character — carries a letter across rather than deleting it:
|
|
14
|
+
* diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
|
|
15
|
+
* `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
|
|
16
|
+
* even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
|
|
17
|
+
* would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
|
|
18
|
+
*
|
|
19
|
+
* Anything still outside printable ASCII after that becomes a space, and runs
|
|
20
|
+
* of whitespace collapse — a space rather than nothing, so a character that
|
|
21
|
+
* transliterates away cannot silently weld two words together.
|
|
22
|
+
*
|
|
23
|
+
* The value is emitted even when it equals the name, so a consumer matching on
|
|
24
|
+
* it never has to branch on whether the name happened to be ASCII already.
|
|
25
|
+
*
|
|
26
|
+
* @param {unknown} name - The note's `name.full`.
|
|
27
|
+
* @returns {string|null} The ASCII form, or `null` when there is no name, or
|
|
28
|
+
* nothing printable survives.
|
|
29
|
+
*/
|
|
30
|
+
export function asciiName(name: unknown): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* The name a note files under: its `name.sort` when it declares one, otherwise
|
|
33
|
+
* its `name.full`.
|
|
34
|
+
*
|
|
35
|
+
* @param {unknown} record - An index record or a note's frontmatter.
|
|
36
|
+
* @returns {string|undefined} The unfolded key, or `undefined` for a record
|
|
37
|
+
* with no name.
|
|
38
|
+
*/
|
|
39
|
+
export function nameSortOf(record: unknown): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The key a name-ordered list compares notes on: {@link nameSortOf} folded to
|
|
42
|
+
* printable ASCII by {@link asciiName}, so an accented sort key files beside
|
|
43
|
+
* its base letters. Case is left for the caller, which decides whether its
|
|
44
|
+
* comparison ignores it.
|
|
45
|
+
*
|
|
46
|
+
* @param {unknown} record - An index record or a note's frontmatter.
|
|
47
|
+
* @returns {string} The key, or "" for a record with no name.
|
|
48
|
+
*/
|
|
49
|
+
export function nameSortKey(record: unknown): string;
|
|
@@ -20,11 +20,13 @@ export function indexMarker(slug: string, main?: boolean): string;
|
|
|
20
20
|
* @param {string[]} [opts.heads] - Leading words (`pdf.indexHeads`) matched
|
|
21
21
|
* literally against the start of a name; a name that opens with one is also
|
|
22
22
|
* listed as `<rest>, <head>`, the head without its trailing space or colon. None when absent.
|
|
23
|
-
* @param {Array<{slug: string, name: string, title: string,
|
|
23
|
+
* @param {Array<{slug: string, name: string, title: string, abbreviation?: string,
|
|
24
|
+
* page: string|number}>} [opts.foreign] -
|
|
24
25
|
* Notes another book prints, listed as `Name: Book title, p. N` once this
|
|
25
|
-
* book mentions them.
|
|
26
|
+
* book mentions them, or `Name: Aelw. p12` when the home book has an
|
|
27
|
+
* abbreviation.
|
|
26
28
|
* @returns {Array<{letter: string, name: string, slug: string, see?: string,
|
|
27
|
-
* foreign?: {title: string, page: string}}>} Terms with `see` set for a
|
|
29
|
+
* foreign?: {title: string, abbreviation?: string, page: string}}>} Terms with `see` set for a
|
|
28
30
|
* cross-reference to the main name it points at.
|
|
29
31
|
*/
|
|
30
32
|
export function indexTerms(entries: object[], { owns, foreign, heads }?: {
|
|
@@ -34,6 +36,7 @@ export function indexTerms(entries: object[], { owns, foreign, heads }?: {
|
|
|
34
36
|
slug: string;
|
|
35
37
|
name: string;
|
|
36
38
|
title: string;
|
|
39
|
+
abbreviation?: string;
|
|
37
40
|
page: string | number;
|
|
38
41
|
}[] | undefined;
|
|
39
42
|
}): Array<{
|
|
@@ -43,6 +46,7 @@ export function indexTerms(entries: object[], { owns, foreign, heads }?: {
|
|
|
43
46
|
see?: string;
|
|
44
47
|
foreign?: {
|
|
45
48
|
title: string;
|
|
49
|
+
abbreviation?: string;
|
|
46
50
|
page: string;
|
|
47
51
|
};
|
|
48
52
|
}>;
|
|
@@ -57,7 +57,7 @@ export function readEntryPages(typPath: string, fontArgs: string[], binary?: str
|
|
|
57
57
|
* The books that render, in configuration order.
|
|
58
58
|
* @param {Map<string, string>} input.homes - Each note's key → its home book's
|
|
59
59
|
* id, from {@link module:engine/pdf-coverage.coverage}.
|
|
60
|
-
* @returns {Array<{key: string, book: string, title: string, anchor: string,
|
|
60
|
+
* @returns {Array<{key: string, book: string, title: string, abbreviation?: string, anchor: string,
|
|
61
61
|
* record: object, slug: string, aliases: string[]}>} One per homed note, in book order and
|
|
62
62
|
* then entry order.
|
|
63
63
|
*/
|
|
@@ -74,6 +74,7 @@ export function homeEntries({ books, homes }: {
|
|
|
74
74
|
key: string;
|
|
75
75
|
book: string;
|
|
76
76
|
title: string;
|
|
77
|
+
abbreviation?: string;
|
|
77
78
|
anchor: string;
|
|
78
79
|
record: object;
|
|
79
80
|
slug: string;
|
|
@@ -166,19 +166,30 @@ export function markdownToTypst(markdown: string, opts?: {
|
|
|
166
166
|
export function absolutePageUrl(url: string, site?: string): string;
|
|
167
167
|
/**
|
|
168
168
|
* A reference to a note another book prints, as Typst: the words the link
|
|
169
|
-
* carried, then the book and the page
|
|
169
|
+
* carried, then the book and the page. A book with an abbreviation reads
|
|
170
|
+
* `Name (Aelw. p12)`; one without reads `Name (_Book title_, p. 12)`.
|
|
170
171
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
172
|
+
* A link the text already closes a bracket on, as in `(see Name)`, takes the
|
|
173
|
+
* reference inside that bracket after a comma, `(see Name, Aelw. p12)`, so the
|
|
174
|
+
* brackets never nest.
|
|
175
|
+
*
|
|
176
|
+
* The space before the page number, or between the abbreviation and the page,
|
|
177
|
+
* does not break, so a number never starts a line of its own.
|
|
173
178
|
*
|
|
174
179
|
* @param {string} inner - The already-rendered link text.
|
|
175
|
-
* @param {{title: string, page: string|number}} away -
|
|
176
|
-
*
|
|
180
|
+
* @param {{title: string, abbreviation?: string, page: string|number}} away -
|
|
181
|
+
* The home book's title, its abbreviation when it declares one, and the page
|
|
182
|
+
* the entry starts on.
|
|
183
|
+
* @param {{bracketed?: boolean}} [opts] - `bracketed` when the text closes a
|
|
184
|
+
* bracket right after the link.
|
|
177
185
|
* @returns {string} Typst markup.
|
|
178
186
|
*/
|
|
179
187
|
export function crossBookReference(inner: string, away: {
|
|
180
188
|
title: string;
|
|
189
|
+
abbreviation?: string;
|
|
181
190
|
page: string | number;
|
|
191
|
+
}, { bracketed }?: {
|
|
192
|
+
bracketed?: boolean;
|
|
182
193
|
}): string;
|
|
183
194
|
/**
|
|
184
195
|
* The Typst definitions the book's page furniture is drawn with.
|
|
@@ -72,7 +72,7 @@ export function runTreeFilters(nodes: object[], db: {
|
|
|
72
72
|
* without a renderer or a PDF. Only how the result *looks* needs eyes.
|
|
73
73
|
*
|
|
74
74
|
* **Notes are sorted, prose is not.** A section's entries come out in
|
|
75
|
-
*
|
|
75
|
+
* name-sort order (`name.sort`, else `name.full`, folded to ASCII), so a
|
|
76
76
|
* circumflex sorts with its letter instead of after `Z` as a raw codepoint
|
|
77
77
|
* comparison would put it. Prose keeps the position it was written in, because
|
|
78
78
|
* its place in the sequence is the author's statement.
|
|
@@ -59,6 +59,11 @@ export type RelatedEntry = {
|
|
|
59
59
|
* The note's `type`, which the theme groups by.
|
|
60
60
|
*/
|
|
61
61
|
type: string;
|
|
62
|
+
/**
|
|
63
|
+
* The note's `name.sort`. It orders the list in place
|
|
64
|
+
* of `title` and is not published.
|
|
65
|
+
*/
|
|
66
|
+
sort?: string | undefined;
|
|
62
67
|
};
|
|
63
68
|
/**
|
|
64
69
|
* The `related` block one page publishes.
|
|
@@ -1,135 +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
|
-
* Asserts that npm serves an exact package version.
|
|
16
|
-
*
|
|
17
|
-
* `npm publish` returns before the registry serves the new version, and a
|
|
18
|
-
* version that is accepted but never served is indistinguishable from success
|
|
19
|
-
* by the publish alone. This polls `npm view <name>@<version> version` until
|
|
20
|
-
* it prints the version, then exits 0. When the time budget runs out it exits 1
|
|
21
|
-
* with an `::error::` annotation naming the package, the version and the last
|
|
22
|
-
* registry answer.
|
|
23
|
-
*
|
|
24
|
-
* Usage: `node ci/confirm-npm-version.mjs <name> <version> [--interval <s>]
|
|
25
|
-
* [--timeout <s>]`. The defaults are 15 seconds between queries and 600 seconds
|
|
26
|
-
* overall.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { spawnSync } from "node:child_process";
|
|
30
|
-
import { fileURLToPath } from "node:url";
|
|
31
|
-
|
|
32
|
-
/** Seconds between registry queries. */
|
|
33
|
-
export const DEFAULT_INTERVAL_SECONDS = 15;
|
|
34
|
-
/** Seconds the registry has to serve the version. */
|
|
35
|
-
export const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Ask the registry which version `name@version` resolves to.
|
|
39
|
-
*
|
|
40
|
-
* @param {string} spec - `<name>@<version>`.
|
|
41
|
-
* @returns {string} The version npm printed, or its error text when the query
|
|
42
|
-
* fails (an unpublished version answers E404).
|
|
43
|
-
*/
|
|
44
|
-
export function queryNpm(spec) {
|
|
45
|
-
const result = spawnSync("npm", ["view", spec, "version"], { encoding: "utf8" });
|
|
46
|
-
if (result.error) return result.error.message;
|
|
47
|
-
const out = (result.stdout ?? "").trim();
|
|
48
|
-
if (result.status === 0) return out;
|
|
49
|
-
const err = (result.stderr ?? "").trim().replace(/\s*\n\s*/g, " | ");
|
|
50
|
-
return err || out || `npm exited ${result.status}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Poll until the registry serves `version` of `name`.
|
|
55
|
-
*
|
|
56
|
-
* @param {object} options
|
|
57
|
-
* @param {string} options.name - Package name.
|
|
58
|
-
* @param {string} options.version - Exact version expected.
|
|
59
|
-
* @param {(spec: string) => string} [options.query] - Registry query; returns
|
|
60
|
-
* the registry's answer for `<name>@<version>`.
|
|
61
|
-
* @param {(ms: number) => Promise<void>} [options.sleep] - Waits between queries.
|
|
62
|
-
* @param {number} [options.intervalSeconds] - Seconds between queries.
|
|
63
|
-
* @param {number} [options.timeoutSeconds] - Seconds before giving up.
|
|
64
|
-
* @param {(line: string) => void} [options.log] - Progress output.
|
|
65
|
-
* @returns {Promise<{ok: true, attempts: number} | {ok: false, attempts: number, last: string}>}
|
|
66
|
-
*/
|
|
67
|
-
export async function confirmNpmVersion({
|
|
68
|
-
name,
|
|
69
|
-
version,
|
|
70
|
-
query = queryNpm,
|
|
71
|
-
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
72
|
-
intervalSeconds = DEFAULT_INTERVAL_SECONDS,
|
|
73
|
-
timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
|
|
74
|
-
log = () => {},
|
|
75
|
-
}) {
|
|
76
|
-
const spec = `${name}@${version}`;
|
|
77
|
-
const attemptLimit = Math.floor(timeoutSeconds / intervalSeconds) + 1;
|
|
78
|
-
let last = "";
|
|
79
|
-
for (let attempts = 1; attempts <= attemptLimit; attempts++) {
|
|
80
|
-
last = query(spec);
|
|
81
|
-
if (last === version) {
|
|
82
|
-
log(`${spec} is served by npm.`);
|
|
83
|
-
return { ok: true, attempts };
|
|
84
|
-
}
|
|
85
|
-
log(`${spec} is not served yet (${last || "empty answer"}).`);
|
|
86
|
-
if (attempts < attemptLimit) await sleep(intervalSeconds * 1000);
|
|
87
|
-
}
|
|
88
|
-
return { ok: false, attempts: attemptLimit, last };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function parseArguments(argv) {
|
|
92
|
-
const positional = [];
|
|
93
|
-
const options = {};
|
|
94
|
-
for (let i = 0; i < argv.length; i++) {
|
|
95
|
-
const arg = argv[i];
|
|
96
|
-
if (arg === "--interval" || arg === "--timeout") {
|
|
97
|
-
const value = Number(argv[++i]);
|
|
98
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
99
|
-
throw new Error(`${arg} takes a positive number of seconds.`);
|
|
100
|
-
}
|
|
101
|
-
options[arg === "--interval" ? "intervalSeconds" : "timeoutSeconds"] = value;
|
|
102
|
-
} else if (arg.startsWith("--")) {
|
|
103
|
-
throw new Error(`Unknown option ${arg}.`);
|
|
104
|
-
} else {
|
|
105
|
-
positional.push(arg);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (positional.length !== 2) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
"Usage: confirm-npm-version.mjs <name> <version> [--interval <s>] [--timeout <s>]",
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
return { name: positional[0], version: positional[1], ...options };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function main(argv) {
|
|
117
|
-
let options;
|
|
118
|
-
try {
|
|
119
|
-
options = parseArguments(argv);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error(`::error::${error.message}`);
|
|
122
|
-
return 2;
|
|
123
|
-
}
|
|
124
|
-
const outcome = await confirmNpmVersion({ ...options, log: (line) => console.log(line) });
|
|
125
|
-
if (outcome.ok) return 0;
|
|
126
|
-
const timeout = options.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
127
|
-
console.error(
|
|
128
|
-
`::error::npm does not serve ${options.name}@${options.version} after ${timeout}s and ${outcome.attempts} queries. Last registry answer: ${outcome.last || "(empty)"}`,
|
|
129
|
-
);
|
|
130
|
-
return 1;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
134
|
-
process.exitCode = await main(process.argv.slice(2));
|
|
135
|
-
}
|