@heroiclands/package-build 22.15.0 → 22.16.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 +103 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +303 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +294 -234
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +33 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +631 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4420 -0
- package/docs/reference/note-types.md +531 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-human.mjs +71 -0
- package/engine/calendar-notes.mjs +41 -3
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +745 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/itinerary-scenes.mjs +4 -2
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/map-raster.mjs +4 -2
- package/engine/markdown-expressions.mjs +117 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +283 -61
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +205 -93
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +2 -0
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +4 -2
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
package/engine/sql-tables.mjs
CHANGED
|
@@ -27,15 +27,17 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { parseAddress, renderAddress, isAddressTuple } from "./address.mjs";
|
|
30
|
-
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
30
|
+
import { NOTE_VOCABULARY, isGmNote } from "./note-vocabulary.mjs";
|
|
31
31
|
import { encodeAddresses } from "./address-values.mjs";
|
|
32
32
|
import fs from "node:fs";
|
|
33
33
|
import os from "node:os";
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
|
|
36
36
|
import { FENCE_LINE, parseHeaderArgs } from "./code-fences.mjs";
|
|
37
|
+
import { booleanAttribute } from "./extension-attributes.mjs";
|
|
37
38
|
import { MARKET_CLASSES } from "./market-class.mjs";
|
|
38
39
|
import { parseMarkdownFile } from "./helpers.mjs";
|
|
40
|
+
import { sqlQueriesInMarkdown } from "./markdown-expressions.mjs";
|
|
39
41
|
// The record accessors only — see `engine/index-records.mjs`.
|
|
40
42
|
import { isNoteRecord, noteFile, sortKeysDeep } from "./index-records.mjs";
|
|
41
43
|
|
|
@@ -79,7 +81,7 @@ export function findSqlBlocks(markdown) {
|
|
|
79
81
|
const closer = new RegExp(`^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`);
|
|
80
82
|
let close = i + 1;
|
|
81
83
|
while (close < lines.length && !closer.test(lines[close])) close += 1;
|
|
82
|
-
const { language, args } = parseHeaderArgs(info);
|
|
84
|
+
const { language, args, problems } = parseHeaderArgs(info);
|
|
83
85
|
if (language !== "sql") {
|
|
84
86
|
// Not ours, but still a fence: skip its body so a `sql` line inside
|
|
85
87
|
// some other block is never read as a directive.
|
|
@@ -87,20 +89,30 @@ export function findSqlBlocks(markdown) {
|
|
|
87
89
|
continue;
|
|
88
90
|
}
|
|
89
91
|
if (close >= lines.length) continue;
|
|
90
|
-
const level = Number(args["section-level"]);
|
|
92
|
+
const level = Number(args["section-level"] ?? 2);
|
|
93
|
+
const errors = [...problems];
|
|
94
|
+
for (const key of Object.keys(args)) {
|
|
95
|
+
if (!["allow-empty", "section-level"].includes(key))
|
|
96
|
+
errors.push(`${key} is not an SQL fence attribute`);
|
|
97
|
+
}
|
|
98
|
+
let allowEmpty = false;
|
|
99
|
+
if (Object.hasOwn(args, "allow-empty")) {
|
|
100
|
+
try {
|
|
101
|
+
allowEmpty = booleanAttribute(args["allow-empty"], "allow-empty");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
errors.push(error.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!Number.isInteger(level) || level < 1 || level > 6)
|
|
107
|
+
errors.push("section-level needs an integer from 1 through 6");
|
|
91
108
|
blocks.push({
|
|
92
109
|
line: i,
|
|
93
110
|
close,
|
|
94
111
|
indent,
|
|
95
112
|
query: lines.slice(i + 1, close).join("\n"),
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
allowEmpty: args["allow-empty"] === true,
|
|
100
|
-
sectionLevel: Number.isInteger(level) && level >= 1 && level <= 6 ? level : 2,
|
|
101
|
-
// Every header argument, so a caller can read one this module makes
|
|
102
|
-
// no use of — the point of taking a real grammar rather than a
|
|
103
|
-
// regex per property.
|
|
113
|
+
allowEmpty,
|
|
114
|
+
sectionLevel: level,
|
|
115
|
+
problems: errors,
|
|
104
116
|
args,
|
|
105
117
|
block: lines.slice(i, close + 1).join("\n"),
|
|
106
118
|
});
|
|
@@ -150,10 +162,16 @@ export function findSqlBlocks(markdown) {
|
|
|
150
162
|
* @param {string} [opts.dir] - Directory for the temporary file.
|
|
151
163
|
* @param {Array<{id: string, file: string}>} [opts.dependencies] - Each
|
|
152
164
|
* declared dependency's cached index, attached as a schema named `id`.
|
|
165
|
+
* @param {object} [opts.addressContext] - Address resolution context.
|
|
166
|
+
* @param {"all"|"public"} [opts.audience] - Whether to exclude GM notes from
|
|
167
|
+
* local and dependency relations.
|
|
153
168
|
* @returns {Promise<{query: (sql: string) => Promise<object[]>,
|
|
154
169
|
* close: () => Promise<void>}>} The open database.
|
|
155
170
|
*/
|
|
156
|
-
export async function openNotesDatabase(
|
|
171
|
+
export async function openNotesDatabase(
|
|
172
|
+
records,
|
|
173
|
+
{ dir, dependencies = [], addressContext, audience = "all" } = {},
|
|
174
|
+
) {
|
|
157
175
|
const { DuckDBInstance } = await import("@duckdb/node-api");
|
|
158
176
|
const base = dir ?? fs.mkdtempSync(path.join(os.tmpdir(), "content-sql-"));
|
|
159
177
|
fs.mkdirSync(base, { recursive: true });
|
|
@@ -166,7 +184,7 @@ export async function openNotesDatabase(records, { dir, dependencies = [], addre
|
|
|
166
184
|
const instance = await DuckDBInstance.create(":memory:");
|
|
167
185
|
const connection = await instance.connect();
|
|
168
186
|
await connection.run("SET threads=1");
|
|
169
|
-
await createRelations(connection, jsonl);
|
|
187
|
+
await createRelations(connection, jsonl, "", audience);
|
|
170
188
|
|
|
171
189
|
// One schema per declared dependency, so `FROM sohl.notes` reads the notes
|
|
172
190
|
// that package published. Quoted, because a package id may carry a hyphen
|
|
@@ -176,7 +194,7 @@ export async function openNotesDatabase(records, { dir, dependencies = [], addre
|
|
|
176
194
|
if (!dep?.id || !dep?.file || !fs.existsSync(dep.file)) continue;
|
|
177
195
|
const schema = `"${String(dep.id).replace(/"/g, '""')}"`;
|
|
178
196
|
await connection.run(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
|
|
179
|
-
await createRelations(connection, dep.file, `${schema}
|
|
197
|
+
await createRelations(connection, dep.file, `${schema}.`, audience);
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
// The market scale as a relation, so a table prints `village` beside the
|
|
@@ -320,7 +338,7 @@ const WITH_STUBS_SCHEMA = "__with_stubs";
|
|
|
320
338
|
* @param {string} [prefix] - A schema to qualify the view names with.
|
|
321
339
|
* @returns {Promise<void>}
|
|
322
340
|
*/
|
|
323
|
-
async function createRelations(connection, file, prefix = "") {
|
|
341
|
+
async function createRelations(connection, file, prefix = "", audience = "all") {
|
|
324
342
|
const read = readJsonAuto(file);
|
|
325
343
|
const described = await connection.runAndReadAll(`DESCRIBE ${read}`);
|
|
326
344
|
const columns = new Set(described.getRowObjects().map((row) => String(row.column_name)));
|
|
@@ -335,10 +353,15 @@ async function createRelations(connection, file, prefix = "") {
|
|
|
335
353
|
"COALESCE(list_contains(TRY_CAST(tags AS VARCHAR[]), 'draft'), false) " +
|
|
336
354
|
"OR COALESCE(TRY_CAST(tags AS VARCHAR) = 'draft', false)"
|
|
337
355
|
: "false";
|
|
356
|
+
const visible =
|
|
357
|
+
audience === "public" && columns.has("tags") ?
|
|
358
|
+
"NOT (COALESCE(list_contains(TRY_CAST(tags AS VARCHAR[]), 'gm'), false) " +
|
|
359
|
+
"OR COALESCE(TRY_CAST(tags AS VARCHAR) = 'gm', false))"
|
|
360
|
+
: "true";
|
|
338
361
|
await connection.run(
|
|
339
362
|
`CREATE VIEW ${prefix}entries AS SELECT *, ` +
|
|
340
363
|
`CASE WHEN ${stub} THEN 'stub' WHEN ${draft} THEN 'draft' ` +
|
|
341
|
-
`ELSE 'full' END AS state FROM (${read})`,
|
|
364
|
+
`ELSE 'full' END AS state FROM (${read}) WHERE ${visible}`,
|
|
342
365
|
);
|
|
343
366
|
await connection.run(
|
|
344
367
|
`CREATE VIEW ${prefix}notes AS SELECT * FROM ${prefix}entries WHERE state <> 'stub'`,
|
|
@@ -467,6 +490,7 @@ function cellText(value, column) {
|
|
|
467
490
|
* @param {(ref: string) => boolean} [opts.linkable] - Whether an address can be
|
|
468
491
|
* linked to; defaults to linking any non-empty `_ref`.
|
|
469
492
|
* @param {number} [opts.sectionLevel=2] - Heading level for `_section`.
|
|
493
|
+
* @param {object} [opts.addressContext] - Address resolution context.
|
|
470
494
|
* @returns {string} The markdown.
|
|
471
495
|
*/
|
|
472
496
|
export function renderSqlTable(
|
|
@@ -563,6 +587,7 @@ export async function prepareSqlTables(db, sources, { linkable } = {}) {
|
|
|
563
587
|
prepared.set(source, forNote);
|
|
564
588
|
for (const block of blocks) {
|
|
565
589
|
try {
|
|
590
|
+
if (block.problems.length) throw new Error(block.problems.join("; "));
|
|
566
591
|
const result = await runSqlQuery(db, block.query);
|
|
567
592
|
forNote.push({
|
|
568
593
|
markdown: renderSqlTable(result, {
|
|
@@ -589,6 +614,42 @@ export async function prepareSqlTables(db, sources, { linkable } = {}) {
|
|
|
589
614
|
return prepared;
|
|
590
615
|
}
|
|
591
616
|
|
|
617
|
+
/** Evaluate scalar SQL calls before synchronous Markdown renderers run. */
|
|
618
|
+
export async function prepareInlineSqlExpressions(db, sources) {
|
|
619
|
+
const byNote = new Map();
|
|
620
|
+
const cache = new Map();
|
|
621
|
+
for (const { source, markdown, frontmatter } of sources) {
|
|
622
|
+
const queries = sqlQueriesInMarkdown(markdown, frontmatter);
|
|
623
|
+
if (!queries.length) continue;
|
|
624
|
+
const values = new Map();
|
|
625
|
+
byNote.set(source, values);
|
|
626
|
+
for (const query of queries) {
|
|
627
|
+
if (!cache.has(query)) {
|
|
628
|
+
try {
|
|
629
|
+
const result = await db.query(query);
|
|
630
|
+
if (result.columnNames.length !== 1 || result.rows.length !== 1)
|
|
631
|
+
throw new RangeError("scalar SQL needs exactly one column and one row");
|
|
632
|
+
const value = result.rows[0][result.columnNames[0]];
|
|
633
|
+
if (value === null || value === undefined || value === "")
|
|
634
|
+
throw new RangeError("scalar SQL result is empty");
|
|
635
|
+
if (
|
|
636
|
+
value !== null &&
|
|
637
|
+
!["string", "number", "boolean", "bigint"].includes(typeof value)
|
|
638
|
+
)
|
|
639
|
+
throw new TypeError(
|
|
640
|
+
"scalar SQL result must be a number, string, Boolean, or null",
|
|
641
|
+
);
|
|
642
|
+
cache.set(query, { value });
|
|
643
|
+
} catch (error) {
|
|
644
|
+
cache.set(query, { error: String(error?.message ?? error).split("\n")[0] });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
values.set(query, cache.get(query));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return byNote;
|
|
651
|
+
}
|
|
652
|
+
|
|
592
653
|
/**
|
|
593
654
|
* Answer every `sql` directive in a content tree.
|
|
594
655
|
*
|
|
@@ -609,16 +670,22 @@ export async function prepareSqlTables(db, sources, { linkable } = {}) {
|
|
|
609
670
|
* @param {object[]} [opts.records] - Index records the caller already derived.
|
|
610
671
|
* A command that also builds a link index holds them already, and deriving
|
|
611
672
|
* them twice is the duplicated-corpus failure this closes.
|
|
673
|
+
* @param {"all"|"public"} [opts.audience] - Whether to exclude GM notes.
|
|
612
674
|
* @returns {Promise<Map<string, object[]>|undefined>} Results by note path, or
|
|
613
675
|
* nothing when the tree has no such directive.
|
|
614
676
|
*/
|
|
615
|
-
export async function prepareTreeSqlTables(
|
|
677
|
+
export async function prepareTreeSqlTables(
|
|
678
|
+
contentBase,
|
|
679
|
+
{ config, skipDirectories, records, audience = "all" } = {},
|
|
680
|
+
) {
|
|
616
681
|
// Imported here rather than at module scope: the index reaches the pack
|
|
617
682
|
// compilers through `manifest-emit` → `journals`, so a static import from a
|
|
618
683
|
// module they load would close a cycle and leave `BasePackCompiler`
|
|
619
684
|
// uninitialised for whichever module the runtime happened to load first.
|
|
620
685
|
const { indexRecordsFor } = await import("./content-index.mjs");
|
|
621
|
-
const indexRecords =
|
|
686
|
+
const indexRecords = (
|
|
687
|
+
records ?? indexRecordsFor({ contentBase, config, skipDirectories })
|
|
688
|
+
).filter((record) => audience !== "public" || !isGmNote(record));
|
|
622
689
|
|
|
623
690
|
// Which notes carry a directive, discovered over the same corpus every
|
|
624
691
|
// other pass reads rather than over a walk of this one's own. The
|
|
@@ -633,8 +700,9 @@ export async function prepareTreeSqlTables(contentBase, { config, skipDirectorie
|
|
|
633
700
|
for (const record of indexRecords) {
|
|
634
701
|
if (!isNoteRecord(record)) continue;
|
|
635
702
|
const absPath = noteFile(contentBase, record);
|
|
636
|
-
const { body } = parseMarkdownFile(absPath);
|
|
637
|
-
if (body && findSqlBlocks(body).length
|
|
703
|
+
const { body, frontmatter } = parseMarkdownFile(absPath);
|
|
704
|
+
if (body && (findSqlBlocks(body).length || sqlQueriesInMarkdown(body, frontmatter).length))
|
|
705
|
+
sources.push({ source: absPath, markdown: body, frontmatter });
|
|
638
706
|
}
|
|
639
707
|
if (!sources.length) return undefined;
|
|
640
708
|
// A cell links only where the address it would emit resolves, so a table
|
|
@@ -664,11 +732,13 @@ export async function prepareTreeSqlTables(contentBase, { config, skipDirectorie
|
|
|
664
732
|
} catch {
|
|
665
733
|
dependencies = [];
|
|
666
734
|
}
|
|
667
|
-
const db = await openNotesDatabase(indexRecords, { dependencies });
|
|
735
|
+
const db = await openNotesDatabase(indexRecords, { dependencies, audience });
|
|
668
736
|
try {
|
|
669
|
-
|
|
737
|
+
const prepared = await prepareSqlTables(db, sources, {
|
|
670
738
|
linkable: (ref) => addresses.has(renderAddress(ref)),
|
|
671
739
|
});
|
|
740
|
+
prepared.inline = await prepareInlineSqlExpressions(db, sources);
|
|
741
|
+
return prepared;
|
|
672
742
|
} finally {
|
|
673
743
|
await db.close();
|
|
674
744
|
}
|
package/engine/web-wikilinks.mjs
CHANGED
|
@@ -123,19 +123,8 @@ function lookupRead(index, read, contentPackage) {
|
|
|
123
123
|
/**
|
|
124
124
|
* How an **unresolved** link renders.
|
|
125
125
|
*
|
|
126
|
-
* The author's text
|
|
127
|
-
*
|
|
128
|
-
* was meant to be a link, and an author can find it: the appearance lives in
|
|
129
|
-
* `scss/components/_unresolved-link.scss` for Foundry and in the Hugo theme for
|
|
130
|
-
* the website, not here.
|
|
131
|
-
*
|
|
132
|
-
* This is deliberately identical to the pack compiler's own `unresolvedLink`,
|
|
133
|
-
* down to the class name and the `title` wording. One authored link renders on
|
|
134
|
-
* two surfaces, and the two builds have drifted before over exactly this kind
|
|
135
|
-
* of detail — matching markup is what keeps a reader's cue the same in
|
|
136
|
-
* a journal and on the page. Duplicated rather than imported only because the
|
|
137
|
-
* function is not exported from `@heroiclands/package-build`; hoisting it there
|
|
138
|
-
* is.
|
|
126
|
+
* The author's text remains readable. The website uses an HTML span matching
|
|
127
|
+
* the pack compiler's cue; the book uses text that Typst can print.
|
|
139
128
|
*
|
|
140
129
|
* The knowledgebase renders with `unsafe = true` (`kb/hugo.toml`), so raw HTML
|
|
141
130
|
* in generated markdown reaches the page. That makes escaping obligatory: this
|
|
@@ -143,9 +132,11 @@ function lookupRead(index, read, contentPackage) {
|
|
|
143
132
|
*
|
|
144
133
|
* @param {string} text - The text to show, from the link's label or target.
|
|
145
134
|
* @param {string} target - The address that resolved nowhere, for the tooltip.
|
|
146
|
-
* @
|
|
135
|
+
* @param {"html"|"book"} output - The destination format.
|
|
136
|
+
* @returns {string} A visible cue in the destination's Markdown.
|
|
147
137
|
*/
|
|
148
|
-
function unresolvedLink(text, target) {
|
|
138
|
+
function unresolvedLink(text, target, output) {
|
|
139
|
+
if (output === "book") return `${text} (unresolved link)`;
|
|
149
140
|
const esc = (v) =>
|
|
150
141
|
String(v)
|
|
151
142
|
.replace(/&/g, "&")
|
|
@@ -159,27 +150,18 @@ function unresolvedLink(text, target) {
|
|
|
159
150
|
}
|
|
160
151
|
|
|
161
152
|
/**
|
|
162
|
-
* How a link to a **draft** note
|
|
153
|
+
* How the website marks a link to a **draft** note.
|
|
163
154
|
*
|
|
164
155
|
* A note tagged `draft` exists so a link into it is not dead, and nothing more.
|
|
165
156
|
* Unmarked, a reader follows a promising link into an empty page and an author
|
|
166
157
|
* cannot see which of their links still owe content.
|
|
167
158
|
*
|
|
168
|
-
*
|
|
169
|
-
* untouched — Goldmark parses inline markdown inside an inline HTML span, so
|
|
170
|
-
* the markdown link still becomes an anchor, and the note is on the site, in
|
|
171
|
-
* the packs and in the manifest exactly as any other. Nothing here resembles
|
|
172
|
-
* the retired `draft:` field, which moved a note from published to unresolvable
|
|
173
|
-
* without saying so.
|
|
159
|
+
* The link itself remains intact inside the HTML span.
|
|
174
160
|
*
|
|
175
161
|
* The appearance lives in the Hugo theme for the website and in
|
|
176
162
|
* `scss/components/_draft-link.scss` for Foundry, not here.
|
|
177
163
|
*
|
|
178
|
-
*
|
|
179
|
-
* class name and the `title` wording — one authored link renders on two
|
|
180
|
-
* surfaces, and the two builds have drifted before over exactly this kind of
|
|
181
|
-
* detail. Duplicated rather than imported for the same reason
|
|
182
|
-
* {@link unresolvedLink} is; hoisting both is.
|
|
164
|
+
* The website span matches the pack compiler's copy in `wikilinks.mjs`.
|
|
183
165
|
*
|
|
184
166
|
* The argument is already-built markup and is deliberately not escaped; the
|
|
185
167
|
* *authored* text inside it was escaped, or made into a link, by the caller.
|
|
@@ -322,7 +304,8 @@ function isPlainMap(value) {
|
|
|
322
304
|
*
|
|
323
305
|
* @param {string} body - The markdown body.
|
|
324
306
|
* @param {object} ctx - `{ index, assets, collide, contentTypes,
|
|
325
|
-
* packages, noIndexPackages, foreign, type, errors, src, file, resolved
|
|
307
|
+
* packages, noIndexPackages, foreign, type, errors, src, file, resolved,
|
|
308
|
+
* output }`. `output` is `"book"` for PDF Markdown and defaults to HTML.
|
|
326
309
|
* `packages` is every package an address may name, without which the leading
|
|
327
310
|
* package segment of a canonical address reads as an unknown type;
|
|
328
311
|
* `noIndexPackages` is every package declared `contentIndex: false`, so a
|
|
@@ -373,7 +356,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
373
356
|
*/
|
|
374
357
|
const report = (all, finding, text) => {
|
|
375
358
|
record(all, finding);
|
|
376
|
-
return unresolvedLink(text, finding.target);
|
|
359
|
+
return unresolvedLink(text, finding.target, ctx.output ?? "html");
|
|
377
360
|
};
|
|
378
361
|
|
|
379
362
|
// An embed names a file, so it resolves before anything looks for a link.
|
|
@@ -416,7 +399,7 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
416
399
|
|
|
417
400
|
// `[[#section-slug|Text]]` — a section of this same page.
|
|
418
401
|
if (isSamePage({ target, anchor })) {
|
|
419
|
-
return `[${label ?? anchor}](#${slugify(anchor)})`;
|
|
402
|
+
return `[${label ?? ctx.captionLabels?.get(anchor) ?? anchor}](#${slugify(anchor)})`;
|
|
420
403
|
}
|
|
421
404
|
|
|
422
405
|
// The canonical separator has to be resolved, not merely
|
|
@@ -455,7 +438,8 @@ export function resolveWebWikilinks(body, ctx) {
|
|
|
455
438
|
// Presentation only — the href above is unchanged, and a
|
|
456
439
|
// `[[#anchor]]` self-link is not marked because the reader is
|
|
457
440
|
// already on the page it would be telling them about.
|
|
458
|
-
|
|
441
|
+
if (!hit.draft) return link;
|
|
442
|
+
return ctx.output === "book" ? `${link} (draft)` : draftLink(link);
|
|
459
443
|
}
|
|
460
444
|
|
|
461
445
|
// **An address resolving nowhere is a failure, unconditionally**.
|
|
@@ -203,6 +203,7 @@ export const LINK_FINDING_REASONS = Object.freeze(
|
|
|
203
203
|
"not-an-asset",
|
|
204
204
|
"unresolved",
|
|
205
205
|
"stub",
|
|
206
|
+
"gm",
|
|
206
207
|
"ambiguous",
|
|
207
208
|
"unknown-anchor",
|
|
208
209
|
"no-content-index",
|
|
@@ -232,7 +233,7 @@ export function unresolvedAddressMessage(target) {
|
|
|
232
233
|
return (
|
|
233
234
|
`address [[${target}]] resolves to no note — no package publishes ` +
|
|
234
235
|
`it. Fix the shortcode, or declare the package that does as a ` +
|
|
235
|
-
`dependency and run \`
|
|
236
|
+
`dependency and run \`package-build deps fetch\``
|
|
236
237
|
);
|
|
237
238
|
}
|
|
238
239
|
|
|
@@ -344,6 +345,8 @@ export function linkFindingMessage({ reason, target, packages, anchor, type, stu
|
|
|
344
345
|
return unresolvedAddressMessage(target);
|
|
345
346
|
case "stub":
|
|
346
347
|
return stubAddressMessage(target, stub);
|
|
348
|
+
case "gm":
|
|
349
|
+
return `address [[${target}]] names a GM-only note from a player-facing note`;
|
|
347
350
|
case "no-content-index":
|
|
348
351
|
return (
|
|
349
352
|
`address [[${target}]] names a package declared \`contentIndex: false\` — ` +
|
package/engine/wikilinks.mjs
CHANGED
|
@@ -121,6 +121,8 @@ export function anchorPageId(noteId, anchorSlug) {
|
|
|
121
121
|
* `contentIndex: false` — a Foundry dependency only. A link naming one fails
|
|
122
122
|
* with `no-content-index` rather than resolving, ambiguously, as either a
|
|
123
123
|
* typo or an undeclared package.
|
|
124
|
+
* @param {Map<string, object>} [opts.referenceTargets] - Local reference targets.
|
|
125
|
+
* @param {Map<string, object>} [opts.foreignReferences] - Foreign reference targets.
|
|
124
126
|
* @returns {{byShortcode: Map<string, object>, types: Set<string>}} `types` is
|
|
125
127
|
* every type the tree actually contains, so a qualifier naming no real type
|
|
126
128
|
* can be told apart from a missing target.
|
|
@@ -158,11 +160,9 @@ export function buildWikilinkIndex(
|
|
|
158
160
|
|
|
159
161
|
uuidByDoc.set(
|
|
160
162
|
d,
|
|
161
|
-
// A note
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// rather than left out, so the address still resolves and a link
|
|
165
|
-
// to it is never reported as dead.
|
|
163
|
+
// A note with no single Foundry document has no compendium UUID:
|
|
164
|
+
// a link to it retains its label in a journal. Keep the indexed
|
|
165
|
+
// address so the link resolves without a dead-link finding.
|
|
166
166
|
d.none ?
|
|
167
167
|
{ uuid: undefined, docUuid: undefined }
|
|
168
168
|
: {
|
|
@@ -375,6 +375,7 @@ function draftLink(inner) {
|
|
|
375
375
|
* entry landed in.
|
|
376
376
|
* @param {{byShortcode: Map, types: Set}} ctx.index - From
|
|
377
377
|
* {@link buildWikilinkIndex}.
|
|
378
|
+
* @param {Map<string, string>} [ctx.captionLabels] - Numbered caption references.
|
|
378
379
|
* @returns {{markdown: string, unresolved: Array<{link: string, target: string,
|
|
379
380
|
* offset: number, reason: string, packages?: string[], anchor?: string}>}}
|
|
380
381
|
* Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all
|
|
@@ -383,7 +384,7 @@ function draftLink(inner) {
|
|
|
383
384
|
* position in `markdown`, which is what lets a caller report the line and
|
|
384
385
|
* column it sits on.
|
|
385
386
|
*/
|
|
386
|
-
export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
387
|
+
export function convertWikilinks(markdown, { type, id, pack, docPack, index, captionLabels }) {
|
|
387
388
|
const unresolved = [];
|
|
388
389
|
|
|
389
390
|
// `offset` is the third replacer argument because the pattern has exactly
|
|
@@ -512,7 +513,8 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
512
513
|
// the document's **current** name stands in and a rename shows at every
|
|
513
514
|
// citation with no link edited. The knowledgebase build reads
|
|
514
515
|
// the same authored link the same way.
|
|
515
|
-
if (!text)
|
|
516
|
+
if (!text)
|
|
517
|
+
text = (target === "" && slug ? captionLabels?.get(slug) : null) ?? doc.name ?? target;
|
|
516
518
|
|
|
517
519
|
// Both addresses were computed when the target was indexed. An item
|
|
518
520
|
// doc lives in the journals pack under its own derived entry id, and
|
|
@@ -526,12 +528,8 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
526
528
|
docUuid: compendiumUuid(index.packageId, "doc", itemDocEntryId(doc.id), doc.docPack),
|
|
527
529
|
};
|
|
528
530
|
const entryUuid = itemDoc ? addresses.docUuid : addresses.uuid;
|
|
529
|
-
// The target
|
|
530
|
-
//
|
|
531
|
-
// a dead link and must not fail the build; but a compendium has
|
|
532
|
-
// nothing to open, so the reader gets the prose and no `@UUID`. The
|
|
533
|
-
// mirror of what the website does for an address that publishes a
|
|
534
|
-
// document and no page.
|
|
531
|
+
// The target has no single Foundry document. Its address is real, but
|
|
532
|
+
// a compendium has nothing to open, so the reader gets the label.
|
|
535
533
|
if (entryUuid === undefined) return text;
|
|
536
534
|
const entryId = itemDoc ? itemDocEntryId(doc.id) : doc.id;
|
|
537
535
|
const isJournal = itemDoc || packForType(doc.type).docType === "JournalEntry";
|
|
@@ -542,7 +540,7 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
|
|
|
542
540
|
// can never hold one is what made such links dead-end; an
|
|
543
541
|
// an item's pages are addressed through its `note` Address.
|
|
544
542
|
// A `#section` the target declares no heading for. Checked here, and
|
|
545
|
-
// not only by `
|
|
543
|
+
// not only by `package-build links`, because this is the build that
|
|
546
544
|
// *emits* the link: `anchorPageId` will hash any slug into a page id,
|
|
547
545
|
// so an undeclared one compiles to a `@UUID` that dead-ends for the
|
|
548
546
|
// reader. A foreign anchor has always been checked this way —
|
package/hm3/item-fields.mjs
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* are written bare, exactly as SoHL's are: the `data:` region is the sweep's
|
|
41
41
|
* migration and neither half has moved to it. Spelling HM3's differently would
|
|
42
42
|
* make a note feeding both systems author the same fact twice for no gain, and
|
|
43
|
-
* would take these rows out of reach of `
|
|
43
|
+
* would take these rows out of reach of `package-build content-format fields`,
|
|
44
44
|
* which pairs a `data.<key>` claim with a bare declared `<key>`. Both halves
|
|
45
45
|
* move together when the sweep lands.
|
|
46
46
|
*
|
package/hm3/items.mjs
CHANGED
|
@@ -12,44 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* code the SoHL pass runs: which notes are claimed, which subtype each becomes,
|
|
20
|
-
* which registry builds it, the authored `hm3.system` passthrough, the schema
|
|
21
|
-
* check, and the compendium envelope. A second system is a map and a handful of
|
|
22
|
-
* emitted keys, which is the arrangement the mapping tables build towards.
|
|
23
|
-
*
|
|
24
|
-
* **What HM3's compiler writes on every item: one key, and only when there is
|
|
25
|
-
* something to write.** The content format gives an item's `{#appearance}`
|
|
26
|
-
* section a home in HM3 — `description` — and SoHL none, since no SoHL Item
|
|
27
|
-
* subtype declares such a field. So the section is rendered here and nowhere
|
|
28
|
-
* else. Where a note has no such section nothing is emitted at all, which
|
|
29
|
-
* matters for `armorlocation`: it is the one HM3 subtype that extends the
|
|
30
|
-
* Foundry base directly, declaring neither `description` nor `notes`, so an
|
|
31
|
-
* unconditional key would be a finding on every armour-location document in the
|
|
32
|
-
* pack.
|
|
33
|
-
*
|
|
34
|
-
* **And one flag, for the fact HM3's `system` has no field for.** The template
|
|
35
|
-
* priority is a shared statement — a note declaring `data.templatePriority` says
|
|
36
|
-
* the same thing to both systems — but HM3 declares no `system` field for it, so
|
|
37
|
-
* it lands under this system's own flag scope as `flags.hm3.templatePriority`,
|
|
38
|
-
* exactly as the Actor pass writes it. Without it, an item note declaring the
|
|
39
|
-
* priority compiles into a SoHL item that knows it is a template and an HM3
|
|
40
|
-
* item that does not.
|
|
41
|
-
*
|
|
42
|
-
* **There is no HM3 equivalent of `docHtml`.** SoHL points an item at the
|
|
43
|
-
* JournalEntry its prose compiled into, and HM3's data model has nowhere to put
|
|
44
|
-
* such a pointer; inventing one would emit a key Foundry discards at load
|
|
45
|
-
* without a word. The prose still compiles into its JournalEntry — the journals
|
|
46
|
-
* pass claims every doc-carrying type regardless of system — it simply is not
|
|
47
|
-
* addressed from the item.
|
|
15
|
+
* HM3's Item compiler uses the shared note-to-Item pass. It puts the pointer
|
|
16
|
+
* to the note's `{#description}` JournalEntryPage in `system.description` when
|
|
17
|
+
* the Item subtype declares that field. `armorlocation` has no description.
|
|
18
|
+
* The shared `data.templatePriority` is recorded in HM3 flags.
|
|
48
19
|
*
|
|
49
20
|
* @module
|
|
50
21
|
*/
|
|
51
22
|
|
|
52
|
-
import { renderSection } from "../engine/anchored-sections.mjs";
|
|
53
23
|
import { SystemItemCompiler } from "../engine/item-compiler.mjs";
|
|
54
24
|
import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
|
|
55
25
|
import { templateFlags } from "./template-priority.mjs";
|
|
@@ -58,7 +28,7 @@ import { templateFlags } from "./template-priority.mjs";
|
|
|
58
28
|
* HM3's Item compile pass.
|
|
59
29
|
*
|
|
60
30
|
* Declares HM3's note-type → document-subtype map, which decides the notes this
|
|
61
|
-
* pass claims and what each becomes, the one `system` key HM3 writes
|
|
31
|
+
* pass claims and what each becomes, the one `system` key HM3 writes as a page pointer,
|
|
62
32
|
* and the template-priority flag HM3's data model has no field for. Everything
|
|
63
33
|
* else is {@link module:engine/item-compiler}'s.
|
|
64
34
|
*/
|
|
@@ -74,28 +44,25 @@ export class Hm3Items extends SystemItemCompiler {
|
|
|
74
44
|
/**
|
|
75
45
|
* The `system` keys this pass derives from the note.
|
|
76
46
|
*
|
|
77
|
-
* `description`
|
|
78
|
-
* authoring it writes into a key the compiler
|
|
79
|
-
* may say something else.
|
|
47
|
+
* `description` points to the note's `{#description}` page. A note
|
|
48
|
+
* authoring it writes into a key the compiler derives from the shared body.
|
|
80
49
|
*
|
|
81
50
|
* @type {readonly {key: string, from: string}[]}
|
|
82
51
|
*/
|
|
83
52
|
static derivedSystemKeys = Object.freeze([
|
|
84
|
-
{ key: "description", from: "the note's `{#
|
|
53
|
+
{ key: "description", from: "the note's `{#description}` page" },
|
|
85
54
|
]);
|
|
86
55
|
|
|
87
56
|
/**
|
|
88
|
-
* The `system.*` field HM3 writes
|
|
57
|
+
* The `system.*` field HM3 writes from the shared item-doc pointer.
|
|
89
58
|
*
|
|
90
59
|
* @param {object} fm - The note's frontmatter.
|
|
91
60
|
* @param {object} at - What the pass already knows about this note.
|
|
92
|
-
* @param {string} at.
|
|
93
|
-
* wikilinks resolved.
|
|
61
|
+
* @param {string} at.description - The resolved JournalEntryPage pointer.
|
|
94
62
|
* @returns {object} The shared `system` fields — `description`, or nothing.
|
|
95
63
|
*/
|
|
96
|
-
commonSystem(fm, {
|
|
97
|
-
|
|
98
|
-
return description ? { description } : {};
|
|
64
|
+
commonSystem(fm, { description }) {
|
|
65
|
+
return description && this.itemSubtype(fm) !== "armorlocation" ? { description } : {};
|
|
99
66
|
}
|
|
100
67
|
|
|
101
68
|
/**
|
package/index.mjs
CHANGED
|
@@ -15,20 +15,8 @@
|
|
|
15
15
|
* `@heroiclands/package-build` — the shared toolchain for building and shipping
|
|
16
16
|
* a HeroicLands **Foundry package**, content and all.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* which read `lang/`, `styles/`, `src/` and the manifest template. They were
|
|
21
|
-
* split by *input* on the theory that a module would use either — an adventure
|
|
22
|
-
* shipping only notes needs no bundler, a variant shipping only behavior needs
|
|
23
|
-
* no Markdown pipeline.
|
|
24
|
-
*
|
|
25
|
-
* **No consumer ever used one alone.** All three installed both, and this
|
|
26
|
-
* package depended on the other besides, so the packaging half dragged the
|
|
27
|
-
* content half in regardless. What the boundary actually cost was a
|
|
28
|
-
* configuration file with two owners, two CLIs with a colliding `manifest`
|
|
29
|
-
* command, and a two-repository dance for changes that touched one idea. The
|
|
30
|
-
* two halves are now one package; the split survives as the shape of this
|
|
31
|
-
* barrel and nothing more.
|
|
18
|
+
* Content and package operations share one configuration and one CLI. The
|
|
19
|
+
* exports here provide library entry points for both kinds of work.
|
|
32
20
|
*
|
|
33
21
|
* **Everything exported here is pure.** Functions take source text and return
|
|
34
22
|
* findings or values; discovery, I/O and reporting stay with the caller. That
|
package/manifest.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import fs from "node:fs/promises";
|
|
|
52
52
|
import fsSync from "node:fs";
|
|
53
53
|
import path from "node:path";
|
|
54
54
|
|
|
55
|
-
import { emitDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
|
|
55
|
+
import { emitDiagnostic, formatDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
|
|
56
56
|
import { metadataFileName } from "./engine/metadata-index.mjs";
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -393,7 +393,7 @@ export function packFolderFindings({ packFolders, packs = [] }) {
|
|
|
393
393
|
* package.
|
|
394
394
|
*
|
|
395
395
|
* `relationships` is the one manifest block with a second reader.
|
|
396
|
-
* `@heroiclands/
|
|
396
|
+
* `@heroiclands/package-build` consumes it too, and v1.8.0 added
|
|
397
397
|
* `itemCatalog: true` as an opt-in on a declared dependency: it selects that
|
|
398
398
|
* package's Item packs as a resolution
|
|
399
399
|
* source for the actors pass. That is an instruction to the build, not a fact
|
|
@@ -408,7 +408,7 @@ export function packFolderFindings({ packFolders, packs = [] }) {
|
|
|
408
408
|
* unlikely to be the last.
|
|
409
409
|
*
|
|
410
410
|
* A list is enough, and needs no prefix agreed between the two packages,
|
|
411
|
-
* because the input is already closed:
|
|
411
|
+
* because the input is already closed: package-build normalises a relationship
|
|
412
412
|
* to `id`, `type`, `manifest`, `compatibility` and its own build keys, and
|
|
413
413
|
* rejects anything else at configuration time. A key that reaches here is one
|
|
414
414
|
* the toolchain itself put there.
|
|
@@ -429,7 +429,7 @@ export const BUILD_ONLY_RELATIONSHIP_KEYS = Object.freeze(["itemCatalog"]);
|
|
|
429
429
|
* unread: a key Foundry adds later should not need a release of this package.
|
|
430
430
|
*
|
|
431
431
|
* @param {Record<string, unknown>} relationships - The declared block, as
|
|
432
|
-
*
|
|
432
|
+
* package-build resolved it.
|
|
433
433
|
* @returns {Record<string, unknown>} It, without the build-only keys.
|
|
434
434
|
*/
|
|
435
435
|
export function publishedRelationships(relationships) {
|
|
@@ -455,6 +455,13 @@ function withoutBuildKeys(entry) {
|
|
|
455
455
|
);
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
/** Require the displayed identity Foundry needs to install the package. */
|
|
459
|
+
function requireManifestTitle(config) {
|
|
460
|
+
const title = config.packageBuild?.manifest?.title;
|
|
461
|
+
if (typeof title !== "string" || title.trim() === "")
|
|
462
|
+
throw new Error("`packageBuild.manifest.title` must be a non-empty string.");
|
|
463
|
+
}
|
|
464
|
+
|
|
458
465
|
/**
|
|
459
466
|
* Build a Foundry package manifest from the resolved configuration.
|
|
460
467
|
*
|
|
@@ -483,6 +490,7 @@ function withoutBuildKeys(entry) {
|
|
|
483
490
|
* @returns {object} The manifest, ready to serialise.
|
|
484
491
|
*/
|
|
485
492
|
export function buildManifest({ config, packageJson, artifact, flags }) {
|
|
493
|
+
requireManifestTitle(config);
|
|
486
494
|
// `descriptionHtml` is the authored source of `description` — pulled out
|
|
487
495
|
// so it never survives the spread below under its own name.
|
|
488
496
|
const { descriptionHtml, ...declared } = config.packageBuild?.manifest ?? {};
|
|
@@ -641,6 +649,25 @@ function reportPackFolders(findings, configFile) {
|
|
|
641
649
|
* ship. Nothing is written in that case.
|
|
642
650
|
*/
|
|
643
651
|
export async function writeManifest({ config, packageJson, artifact, outDir, flags, configFile }) {
|
|
652
|
+
try {
|
|
653
|
+
requireManifestTitle(config);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
let text;
|
|
656
|
+
if (configFile) {
|
|
657
|
+
try {
|
|
658
|
+
text = fsSync.readFileSync(configFile, "utf8");
|
|
659
|
+
} catch {
|
|
660
|
+
text = undefined;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const message = formatDiagnostic({
|
|
664
|
+
...(configFile ? { file: configFile } : {}),
|
|
665
|
+
...(text ? positionOfYamlPath(text, ["packageBuild", "manifest"]) : {}),
|
|
666
|
+
severity: "error",
|
|
667
|
+
message: error.message,
|
|
668
|
+
});
|
|
669
|
+
throw Object.assign(new Error(message), { located: true });
|
|
670
|
+
}
|
|
644
671
|
const manifest = buildManifest({ config, packageJson, artifact, flags });
|
|
645
672
|
|
|
646
673
|
const errors = reportPackFolders(
|