@heroiclands/package-build 6.1.0 → 8.0.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 +840 -0
- package/CONTENT.md +21 -1
- package/bin/content-build.mjs +105 -10
- package/bin/package-build.mjs +114 -1
- package/config.mjs +62 -3
- package/content-config.mjs +254 -22
- package/engine/base-compiler.mjs +25 -0
- package/engine/content-links.mjs +132 -27
- package/engine/diagnostics.mjs +61 -1
- package/engine/foreign-catalog.mjs +47 -0
- package/engine/generate.mjs +10 -5
- package/engine/helpers.mjs +38 -0
- package/engine/journals.mjs +8 -1
- package/engine/macros.mjs +2 -0
- package/engine/pack-config.mjs +143 -13
- package/engine/prose-lint.mjs +10 -2
- package/engine/scenes.mjs +2 -2
- package/engine/schema-check.mjs +332 -0
- package/engine/schema-extract.mjs +611 -0
- package/engine/web-wikilinks.mjs +13 -4
- package/engine/wikilink-syntax.mjs +25 -0
- package/engine/wikilinks.mjs +6 -3
- package/manifest.mjs +37 -2
- package/package.json +5 -3
- package/sohl/actors.mjs +23 -10
- package/sohl/item-fields.mjs +0 -35
- package/sohl/items.mjs +1 -1
- package/types/content-config.d.mts +14 -0
- package/types/engine/base-compiler.d.mts +18 -1
- package/types/engine/content-links.d.mts +11 -3
- package/types/engine/diagnostics.d.mts +33 -1
- package/types/engine/foreign-catalog.d.mts +15 -0
- package/types/engine/generate.d.mts +3 -2
- package/types/engine/helpers.d.mts +29 -3
- package/types/engine/journals.d.mts +7 -1
- package/types/engine/pack-config.d.mts +22 -0
- package/types/engine/prose-lint.d.mts +10 -2
- package/types/engine/schema-check.d.mts +176 -0
- package/types/engine/schema-extract.d.mts +61 -0
- package/types/engine/wikilink-syntax.d.mts +24 -0
- package/types/sohl/actors.d.mts +3 -3
package/engine/diagnostics.mjs
CHANGED
|
@@ -289,12 +289,25 @@ export function positionOfLiteral(text, needle, occurrence = 1) {
|
|
|
289
289
|
* resolves to nothing — yields `{}`, so a caller spreads the result and the
|
|
290
290
|
* position is dropped rather than guessed.
|
|
291
291
|
*
|
|
292
|
+
* **`key: true` addresses the declaration rather than the value.** A finding
|
|
293
|
+
* about a *value* — this pack name is not in `packs[]` — belongs on the value,
|
|
294
|
+
* which is the default. A finding that names a **field** — `\`site.sections.x\`
|
|
295
|
+
* is not a recognized option` — sends the reader to look for that field, so the
|
|
296
|
+
* position should be the field's own, and in a flow mapping
|
|
297
|
+
* (`{ title: X, banner: Y }`) the two are different columns on one line. The
|
|
298
|
+
* key's node is found through the same parse, so this stays one locator rather
|
|
299
|
+
* than a second one free to disagree with it.
|
|
300
|
+
*
|
|
292
301
|
* @param {string} text - The document's contents.
|
|
293
302
|
* @param {ReadonlyArray<string|number>} keyPath - Path to the node: map keys as
|
|
294
303
|
* strings, sequence entries as numbers.
|
|
304
|
+
* @param {object} [opts]
|
|
305
|
+
* @param {boolean} [opts.key=false] - Report where the last segment is
|
|
306
|
+
* *declared* rather than where its value sits. Ignored for a sequence entry,
|
|
307
|
+
* which has no key.
|
|
295
308
|
* @returns {{line?: number, column?: number}} Spreadable position fields.
|
|
296
309
|
*/
|
|
297
|
-
export function positionOfYamlPath(text, keyPath) {
|
|
310
|
+
export function positionOfYamlPath(text, keyPath, { key = false } = {}) {
|
|
298
311
|
if (typeof text !== "string" || !text) return {};
|
|
299
312
|
if (!Array.isArray(keyPath) || keyPath.length === 0) return {};
|
|
300
313
|
|
|
@@ -305,6 +318,19 @@ export function positionOfYamlPath(text, keyPath) {
|
|
|
305
318
|
// `keepScalar` returns the Scalar node rather than its value, which is
|
|
306
319
|
// the only form carrying a range.
|
|
307
320
|
node = doc.getIn(keyPath, true);
|
|
321
|
+
if (key && node !== undefined) {
|
|
322
|
+
const last = keyPath[keyPath.length - 1];
|
|
323
|
+
const parent =
|
|
324
|
+
keyPath.length === 1 ?
|
|
325
|
+
doc.contents
|
|
326
|
+
: doc.getIn(keyPath.slice(0, -1), true);
|
|
327
|
+
const pair = parent?.items?.find?.(
|
|
328
|
+
(item) =>
|
|
329
|
+
item?.key != null &&
|
|
330
|
+
String(item.key.value) === String(last),
|
|
331
|
+
);
|
|
332
|
+
if (pair?.key?.range) node = pair.key;
|
|
333
|
+
}
|
|
308
334
|
} catch {
|
|
309
335
|
return {};
|
|
310
336
|
}
|
|
@@ -314,3 +340,37 @@ export function positionOfYamlPath(text, keyPath) {
|
|
|
314
340
|
const { line, col } = counter.linePos(start);
|
|
315
341
|
return { line, column: col };
|
|
316
342
|
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The YAML key path a **dotted field path** addresses.
|
|
346
|
+
*
|
|
347
|
+
* Configuration checks report the offending key as the path a reader would
|
|
348
|
+
* write it — `packs[1].name`, `site.sections.affliction.title` — because that
|
|
349
|
+
* is what the message has to say. {@link positionOfYamlPath} addresses a node
|
|
350
|
+
* by segments instead, so this is the one translation between them: `.`
|
|
351
|
+
* separates map keys, and a bracketed suffix is a sequence index.
|
|
352
|
+
*
|
|
353
|
+
* A path this cannot parse yields `[]`, which {@link positionOfYamlPath} in
|
|
354
|
+
* turn resolves to no position — dropped rather than guessed, as everything
|
|
355
|
+
* else here is.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} field - The dotted path, as a diagnostic spells it.
|
|
358
|
+
* @returns {Array<string|number>} Its segments, sequence indices as numbers.
|
|
359
|
+
*/
|
|
360
|
+
export function yamlKeyPath(field) {
|
|
361
|
+
if (typeof field !== "string" || field === "") return [];
|
|
362
|
+
/** @type {Array<string|number>} */
|
|
363
|
+
const segments = [];
|
|
364
|
+
for (const segment of field.split(".")) {
|
|
365
|
+
const parsed = /^([^[\]]*)((?:\[\d+\])*)$/.exec(segment);
|
|
366
|
+
// Anything else is not a path this understands — a key holding a `[`,
|
|
367
|
+
// or a dot inside a key. Refuse the whole path rather than resolve
|
|
368
|
+
// part of it to a node that is not the one named.
|
|
369
|
+
if (!parsed) return [];
|
|
370
|
+
if (parsed[1] !== "") segments.push(parsed[1]);
|
|
371
|
+
for (const index of parsed[2].matchAll(/\[(\d+)\]/g)) {
|
|
372
|
+
segments.push(Number(index[1]));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return segments;
|
|
376
|
+
}
|
|
@@ -97,6 +97,51 @@ export function catalogDir(config, id, version) {
|
|
|
97
97
|
*/
|
|
98
98
|
const itemsDir = (dir) => path.join(dir, "items");
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The file a system publishes its `system` field sets as (#60).
|
|
102
|
+
*
|
|
103
|
+
* @type {string}
|
|
104
|
+
*/
|
|
105
|
+
export const SCHEMA_ARTIFACT_FILE = "schema.json";
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Where a cached dependency's published schema sits, if it shipped one.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} config - The resolved configuration.
|
|
111
|
+
* @param {string} id - The dependency's package id.
|
|
112
|
+
* @param {string} version - Its resolved version.
|
|
113
|
+
* @returns {string} The path, whether or not it exists.
|
|
114
|
+
*/
|
|
115
|
+
export function cachedSchemaPath(config, id, version) {
|
|
116
|
+
return path.join(catalogDir(config, id, version), SCHEMA_ARTIFACT_FILE);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Keep the dependency's published schema beside its extracted items.
|
|
121
|
+
*
|
|
122
|
+
* **Copied to one known place rather than read from where it landed.** The two
|
|
123
|
+
* fetch paths leave the unpacked archive in different states — a download
|
|
124
|
+
* unzips into `<cache>/package/` and keeps it, while `--from` unzips into a
|
|
125
|
+
* temporary directory and deletes it — so a reader that went looking in the
|
|
126
|
+
* unpacked tree would find the schema for one and not the other, which is the
|
|
127
|
+
* kind of difference that shows up as an unexplained skipped check.
|
|
128
|
+
*
|
|
129
|
+
* Absent is not an error: a system that has not adopted the artifact yet is
|
|
130
|
+
* simply unchecked, and saying so is {@link module:engine/schema-check}'s job
|
|
131
|
+
* rather than the fetch's.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} root - The unpacked package root.
|
|
134
|
+
* @param {string} dir - The dependency's cache directory.
|
|
135
|
+
* @returns {boolean} Whether one was published.
|
|
136
|
+
*/
|
|
137
|
+
function cacheSchemaArtifact(root, dir) {
|
|
138
|
+
const src = path.join(root, SCHEMA_ARTIFACT_FILE);
|
|
139
|
+
if (!fs.existsSync(src)) return false;
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
141
|
+
fs.copyFileSync(src, path.join(dir, SCHEMA_ARTIFACT_FILE));
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
100
145
|
/**
|
|
101
146
|
* Whether a dependency's cache is present and complete.
|
|
102
147
|
*
|
|
@@ -288,6 +333,7 @@ export async function fetchCatalog(config, rel) {
|
|
|
288
333
|
await downloadAndUnzip(download, raw);
|
|
289
334
|
|
|
290
335
|
await extractItemPacks(rel.id, version, manifest, raw, dir);
|
|
336
|
+
cacheSchemaArtifact(raw, dir);
|
|
291
337
|
return dir;
|
|
292
338
|
}
|
|
293
339
|
|
|
@@ -385,6 +431,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
|
|
|
385
431
|
const dir = catalogDir(config, rel.id, version);
|
|
386
432
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
387
433
|
await extractItemPacks(rel.id, version, manifest, root, dir);
|
|
434
|
+
cacheSchemaArtifact(root, dir);
|
|
388
435
|
log.info(`${rel.id}@${version}: cached from ${source}`);
|
|
389
436
|
return dir;
|
|
390
437
|
} finally {
|
package/engine/generate.mjs
CHANGED
|
@@ -49,7 +49,7 @@ import { Actors } from "../sohl/actors.mjs";
|
|
|
49
49
|
import { Macros } from "./macros.mjs";
|
|
50
50
|
import { Scenes } from "./scenes.mjs";
|
|
51
51
|
import {
|
|
52
|
-
|
|
52
|
+
statsForPack,
|
|
53
53
|
loadFolders,
|
|
54
54
|
buildFolderResolver,
|
|
55
55
|
writeFolderDocs,
|
|
@@ -102,8 +102,9 @@ export const packJsonDir = (name, config = loadPackConfig()) =>
|
|
|
102
102
|
* @param {object} [config] - The resolved build configuration. Defaults to this
|
|
103
103
|
* repository's.
|
|
104
104
|
* @returns {string[]} Each Item pack's JSON directory. Empty when the
|
|
105
|
-
* repository ships no items at all
|
|
106
|
-
*
|
|
105
|
+
* repository ships no items at all, which is a legitimate package: the actors
|
|
106
|
+
* pass accepts an empty list and reports an item it cannot resolve per
|
|
107
|
+
* `(type, shortcode)` instead, naming the being (#49).
|
|
107
108
|
*/
|
|
108
109
|
export function itemPackJsonDirs(config = loadPackConfig()) {
|
|
109
110
|
return config.packs
|
|
@@ -246,7 +247,7 @@ export function unsatisfiedPassDependencies(running, config) {
|
|
|
246
247
|
* count (0 on success) and the number of entries it wrote.
|
|
247
248
|
*/
|
|
248
249
|
async function generatePack(
|
|
249
|
-
{ name, type, folders, companions },
|
|
250
|
+
{ name, type, folders, companions, system },
|
|
250
251
|
config,
|
|
251
252
|
router,
|
|
252
253
|
routingReporter,
|
|
@@ -290,7 +291,9 @@ async function generatePack(
|
|
|
290
291
|
companionDests[companion.name] = companionDest;
|
|
291
292
|
}
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
// A folder document belongs to the pack it is written into, so it carries
|
|
295
|
+
// that pack's system rather than the package-wide one (#48).
|
|
296
|
+
writeFolderDocs(folderList, statsForPack(system, config), dest, type);
|
|
294
297
|
|
|
295
298
|
const pack = new packClass({
|
|
296
299
|
contentBase,
|
|
@@ -311,6 +314,8 @@ async function generatePack(
|
|
|
311
314
|
foreignSourceDirs: foreignItemCatalogDirs(config),
|
|
312
315
|
folderResolver: resolver,
|
|
313
316
|
packName: name,
|
|
317
|
+
// Which system this pack's documents are stamped for (#48).
|
|
318
|
+
packSystem: system ?? null,
|
|
314
319
|
docType: type,
|
|
315
320
|
router,
|
|
316
321
|
routingReporter,
|
package/engine/helpers.mjs
CHANGED
|
@@ -335,6 +335,44 @@ export function buildStats(
|
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* The `_stats` block for one pack, stamped with the system that pack is for
|
|
340
|
+
* (#48).
|
|
341
|
+
*
|
|
342
|
+
* **`systemId` travels with `systemVersion`.** They are one decision, so where
|
|
343
|
+
* one is omitted both are. Stamping a per-pack version against a package-wide
|
|
344
|
+
* id would emit `systemId: sohl, systemVersion: 1.6.3` on HM3 documents — a
|
|
345
|
+
* *plausible lie*, which is worse than the missing value #43 fixed, because
|
|
346
|
+
* nothing about it looks wrong.
|
|
347
|
+
*
|
|
348
|
+
* Resolution, in order:
|
|
349
|
+
*
|
|
350
|
+
* 1. The pack's own `system:`, looked up in the `systems:` block. That is the
|
|
351
|
+
* case a module shipping for two systems needs, and the one no
|
|
352
|
+
* package-wide value could express.
|
|
353
|
+
* 2. Failing that, the package-wide `stats` — a package whose packs are all for
|
|
354
|
+
* one system, which is every package that worked before this existed.
|
|
355
|
+
*
|
|
356
|
+
* A pack naming a system is validated against `systems:` at configuration time,
|
|
357
|
+
* so an unresolvable name never reaches here.
|
|
358
|
+
*
|
|
359
|
+
* @param {string|null|undefined} packSystem - The pack's declared `system:`.
|
|
360
|
+
* @param {object} [config] - The resolved configuration.
|
|
361
|
+
* @returns {object} The `_stats` block for that pack.
|
|
362
|
+
*/
|
|
363
|
+
export function statsForPack(packSystem, config = loadPackConfig()) {
|
|
364
|
+
const declared = packSystem ? config.systems?.[packSystem] : null;
|
|
365
|
+
if (!declared) return buildStats(undefined, config);
|
|
366
|
+
return {
|
|
367
|
+
systemId: packSystem,
|
|
368
|
+
systemVersion: declared.compatibility.verified,
|
|
369
|
+
coreVersion: supportedCoreVersion(config),
|
|
370
|
+
createdTime: 0,
|
|
371
|
+
modifiedTime: 0,
|
|
372
|
+
lastModifiedBy: config.stats.lastModifiedBy,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
338
376
|
/** Memoised {@link defaultStats}. */
|
|
339
377
|
let cachedDefaultStats;
|
|
340
378
|
|
package/engine/journals.mjs
CHANGED
|
@@ -230,6 +230,11 @@ export function buildPages(rawPages, entryId, noteName) {
|
|
|
230
230
|
* heading; see {@link splitPages}.
|
|
231
231
|
* @param {string|null} [params.folder] - The folder id, or `null`.
|
|
232
232
|
* @param {object} [params.flags] - Document flags.
|
|
233
|
+
* @param {object} [params.stats] - The `_stats` block to stamp. Passed by the
|
|
234
|
+
* caller because it is a property of the *pack* being written, not of the
|
|
235
|
+
* entry: a module may ship the same content for two systems, and each pack's
|
|
236
|
+
* documents record the system version they were built against (#48). A
|
|
237
|
+
* caller with no pack in hand gets the package-wide block.
|
|
233
238
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
234
239
|
*/
|
|
235
240
|
export function buildJournalEntry({
|
|
@@ -239,6 +244,7 @@ export function buildJournalEntry({
|
|
|
239
244
|
leadName,
|
|
240
245
|
folder = null,
|
|
241
246
|
flags,
|
|
247
|
+
stats = defaultStats(),
|
|
242
248
|
}) {
|
|
243
249
|
const rawPages = splitPages(markdown, leadName);
|
|
244
250
|
const pages = buildPages(rawPages, id, name);
|
|
@@ -250,7 +256,7 @@ export function buildJournalEntry({
|
|
|
250
256
|
ownership: { default: 0 },
|
|
251
257
|
flags: flags || {},
|
|
252
258
|
_id: id,
|
|
253
|
-
_stats:
|
|
259
|
+
_stats: stats,
|
|
254
260
|
_key: `!journal!${id}`,
|
|
255
261
|
};
|
|
256
262
|
}
|
|
@@ -347,6 +353,7 @@ export class Journals extends BasePackCompiler {
|
|
|
347
353
|
leadName: ownsDoc ? name : undefined,
|
|
348
354
|
folder,
|
|
349
355
|
flags: fm.flags,
|
|
356
|
+
stats: this.stats,
|
|
350
357
|
});
|
|
351
358
|
}
|
|
352
359
|
|
package/engine/macros.mjs
CHANGED
|
@@ -322,6 +322,8 @@ export class Macros extends BasePackCompiler {
|
|
|
322
322
|
return buildMacroEntry(fm, {
|
|
323
323
|
command: macroCommand(body, name),
|
|
324
324
|
folder: this.folderResolver(sohlField(fm, "folder", null)),
|
|
325
|
+
// This pack's system, not the package-wide one (#48).
|
|
326
|
+
stats: this.stats,
|
|
325
327
|
});
|
|
326
328
|
}
|
|
327
329
|
|
package/engine/pack-config.mjs
CHANGED
|
@@ -80,7 +80,12 @@ import path from "node:path";
|
|
|
80
80
|
import { createRequire } from "node:module";
|
|
81
81
|
import YAML from "yaml";
|
|
82
82
|
|
|
83
|
-
import { defineConfig } from "../content-config.mjs";
|
|
83
|
+
import { defineConfig, DERIVED_SYSTEM_VERSION } from "../content-config.mjs";
|
|
84
|
+
import {
|
|
85
|
+
formatDiagnostic,
|
|
86
|
+
positionOfYamlPath,
|
|
87
|
+
yamlKeyPath,
|
|
88
|
+
} from "./diagnostics.mjs";
|
|
84
89
|
|
|
85
90
|
/** The stem every consuming repository declares its build under. */
|
|
86
91
|
export const CONFIG_BASENAME = "package-build.config";
|
|
@@ -276,6 +281,33 @@ function shippedSystemVersion(rootDir, input) {
|
|
|
276
281
|
input.relationships ?? {}
|
|
277
282
|
).systems;
|
|
278
283
|
|
|
284
|
+
// The `systems:` block declares without requiring (#48), so it is consulted
|
|
285
|
+
// first: a package that has adopted it needs no relationship, and one that
|
|
286
|
+
// ships for two systems could not express itself through a relationship at
|
|
287
|
+
// all. `requiresSystem` names the package-wide default when there is one;
|
|
288
|
+
// otherwise a single declared system is unambiguous. With several and no
|
|
289
|
+
// gate, there is no package-wide answer — each pack carries its own, and
|
|
290
|
+
// {@link statsForPack} is what reads it.
|
|
291
|
+
const systemsBlock =
|
|
292
|
+
/** @type {Record<string, {compatibility?: {verified?: string}}>} */ (
|
|
293
|
+
input.systems ?? {}
|
|
294
|
+
);
|
|
295
|
+
const systemIds = Object.keys(systemsBlock);
|
|
296
|
+
if (systemIds.length) {
|
|
297
|
+
const chosen =
|
|
298
|
+
typeof input.requiresSystem === "string" ? input.requiresSystem
|
|
299
|
+
: systemIds.length === 1 ? systemIds[0]
|
|
300
|
+
: null;
|
|
301
|
+
if (chosen) {
|
|
302
|
+
const verified = systemsBlock[chosen]?.compatibility?.verified;
|
|
303
|
+
if (typeof verified === "string" && verified.length)
|
|
304
|
+
return verified;
|
|
305
|
+
}
|
|
306
|
+
// Several declared and none required: the package-wide value is
|
|
307
|
+
// deliberately absent rather than one of them picked arbitrarily.
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
279
311
|
// A module that names neither a system nor a relationship with one is
|
|
280
312
|
// system-agnostic on purpose: its packs are core document types carrying no
|
|
281
313
|
// system data, and it installs under any system. There is no version to
|
|
@@ -314,6 +346,95 @@ function shippedSystemVersion(rootDir, input) {
|
|
|
314
346
|
return verified;
|
|
315
347
|
}
|
|
316
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Where in the configuration file a dotted field path was written.
|
|
351
|
+
*
|
|
352
|
+
* Only a YAML configuration has text to resolve a path against. An `.mjs` one
|
|
353
|
+
* is deliberately not searched: JavaScript source fed to a YAML parser is not
|
|
354
|
+
* an error — it parses as *something*, and a path could resolve to a line that
|
|
355
|
+
* has nothing to do with the key. A position that is wrong is worse than none,
|
|
356
|
+
* so the extension decides.
|
|
357
|
+
*
|
|
358
|
+
* A path that names a key the file never declared — a missing required one —
|
|
359
|
+
* has no node of its own. The position then names the **mapping it belongs
|
|
360
|
+
* in**, one level up and no further: that entry is a real node, and it is the
|
|
361
|
+
* one the reader has to edit. Walking further would drift away from the key
|
|
362
|
+
* with each step, so a top-level key that is simply absent gets no position at
|
|
363
|
+
* all.
|
|
364
|
+
*
|
|
365
|
+
* @param {string} configPath - Absolute path of the configuration file.
|
|
366
|
+
* @param {string} field - The dotted path the diagnostic names.
|
|
367
|
+
* @returns {{line?: number, column?: number}} Spreadable position fields,
|
|
368
|
+
* empty when nothing can be established honestly.
|
|
369
|
+
*/
|
|
370
|
+
function positionInConfig(configPath, field) {
|
|
371
|
+
if (!/\.ya?ml$/i.test(configPath)) return {};
|
|
372
|
+
|
|
373
|
+
let text;
|
|
374
|
+
try {
|
|
375
|
+
text = fs.readFileSync(configPath, "utf8");
|
|
376
|
+
} catch {
|
|
377
|
+
return {};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const keyPath = yamlKeyPath(field);
|
|
381
|
+
if (keyPath.length === 0) return {};
|
|
382
|
+
|
|
383
|
+
const declared = positionOfYamlPath(text, keyPath, { key: true });
|
|
384
|
+
if (declared.line !== undefined) return declared;
|
|
385
|
+
if (keyPath.length === 1) return {};
|
|
386
|
+
return positionOfYamlPath(text, keyPath.slice(0, -1), { key: true });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Attach the position of the key a configuration error names.
|
|
391
|
+
*
|
|
392
|
+
* Eighty-one checks across `content-config.mjs` and `config.mjs` report through
|
|
393
|
+
* one `fail()`, which knows the offending key's dotted path and nothing
|
|
394
|
+
* about where it was written. Locating one of them and not the rest would be
|
|
395
|
+
* worse than locating none — a reader would learn that some configuration
|
|
396
|
+
* errors carry a position and could not predict which — so the path rides on
|
|
397
|
+
* the error and every one of them is located here, at the boundary that knows
|
|
398
|
+
* which file was read (#95).
|
|
399
|
+
*
|
|
400
|
+
* The message keeps its body and gains the `file:line:column: error: ` prefix
|
|
401
|
+
* every other finding in this build already uses, so nothing a reader has today
|
|
402
|
+
* is lost. `located` marks it done, so an error crossing two boundaries is
|
|
403
|
+
* decorated once; the fields are also left on the error, for a caller that
|
|
404
|
+
* wants to re-render it.
|
|
405
|
+
*
|
|
406
|
+
* @param {unknown} err - What was thrown.
|
|
407
|
+
* @param {string} [configPath] - The configuration file that was read.
|
|
408
|
+
* @returns {unknown} The same error, decorated when it named a field.
|
|
409
|
+
*/
|
|
410
|
+
export function locateConfigError(err, configPath) {
|
|
411
|
+
const failure =
|
|
412
|
+
/** @type {{field?: unknown, located?: boolean, message?: string, file?: string, line?: number, column?: number}} */ (
|
|
413
|
+
err
|
|
414
|
+
);
|
|
415
|
+
if (!(err instanceof Error)) return err;
|
|
416
|
+
if (
|
|
417
|
+
failure.located ||
|
|
418
|
+
typeof failure.field !== "string" ||
|
|
419
|
+
!failure.field
|
|
420
|
+
) {
|
|
421
|
+
return err;
|
|
422
|
+
}
|
|
423
|
+
if (!configPath) return err;
|
|
424
|
+
|
|
425
|
+
const at = {
|
|
426
|
+
file: configPath,
|
|
427
|
+
...positionInConfig(configPath, failure.field),
|
|
428
|
+
};
|
|
429
|
+
Object.assign(failure, at, { located: true });
|
|
430
|
+
failure.message = formatDiagnostic({
|
|
431
|
+
...at,
|
|
432
|
+
severity: "error",
|
|
433
|
+
message: /** @type {string} */ (failure.message),
|
|
434
|
+
});
|
|
435
|
+
return err;
|
|
436
|
+
}
|
|
437
|
+
|
|
317
438
|
/**
|
|
318
439
|
* Turn a parsed YAML configuration into the frozen one the engine reads.
|
|
319
440
|
*
|
|
@@ -400,23 +521,28 @@ export function configFromData(data, configPath) {
|
|
|
400
521
|
const stats = input.stats;
|
|
401
522
|
if (stats !== null && typeof stats === "object" && !Array.isArray(stats)) {
|
|
402
523
|
const declared = /** @type {Record<string, unknown>} */ (stats);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
524
|
+
// `stats.systemId` and `stats.systemVersion` are both refused by
|
|
525
|
+
// `defineConfig`, which reports them with a locator — so nothing is
|
|
526
|
+
// rejected here. This half only supplies the value the validator cannot
|
|
527
|
+
// compute: resolving a system package's version means reading the
|
|
528
|
+
// adjacent `package.json`, and `defineConfig` performs no I/O.
|
|
529
|
+
//
|
|
530
|
+
// Passed under a symbol so the channel is not a second, forgeable
|
|
531
|
+
// spelling of the key that was just refused (see
|
|
532
|
+
// {@link DERIVED_SYSTEM_VERSION}).
|
|
413
533
|
input.stats = {
|
|
414
534
|
...declared,
|
|
415
|
-
|
|
535
|
+
[DERIVED_SYSTEM_VERSION]: shippedSystemVersion(rootDir, input),
|
|
416
536
|
};
|
|
417
537
|
}
|
|
418
538
|
|
|
419
|
-
|
|
539
|
+
try {
|
|
540
|
+
return defineConfig(/** @type {never} */ (input));
|
|
541
|
+
} catch (err) {
|
|
542
|
+
// Every `fail()` in the validator names a key and knows nothing about
|
|
543
|
+
// the file; this is where the two meet.
|
|
544
|
+
throw locateConfigError(err, configPath);
|
|
545
|
+
}
|
|
420
546
|
}
|
|
421
547
|
|
|
422
548
|
/**
|
|
@@ -432,6 +558,10 @@ function loadCodeConfig(configPath) {
|
|
|
432
558
|
try {
|
|
433
559
|
module = require(configPath);
|
|
434
560
|
} catch (err) {
|
|
561
|
+
// A code configuration calls `defineConfig` itself, so its rejections
|
|
562
|
+
// arrive here. There is no YAML to locate into, but the file is known —
|
|
563
|
+
// `locateConfigError` names it and drops the line.
|
|
564
|
+
locateConfigError(err, configPath);
|
|
435
565
|
if (
|
|
436
566
|
/** @type {{ code?: string }} */ (err)?.code ===
|
|
437
567
|
"ERR_REQUIRE_ASYNC_MODULE"
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -240,8 +240,16 @@ function toDiagnostic(directory, result) {
|
|
|
240
240
|
*
|
|
241
241
|
* {@link MARKDOWNLINT_CONFIG} is passed as markdownlint's `optionsDefault`,
|
|
242
242
|
* which is precisely the "shipped default, consumer overrides" behaviour the
|
|
243
|
-
* command promises: a
|
|
244
|
-
* and a
|
|
243
|
+
* command promises: a repository with no configuration of its own gets these
|
|
244
|
+
* rules, and a `.markdownlint-cli2.jsonc` found in the tree overrides them.
|
|
245
|
+
*
|
|
246
|
+
* The override is **key by key, and each key wholesale** — which is not the same
|
|
247
|
+
* as "replaces it", and the difference is the one worth stating. A consumer file
|
|
248
|
+
* declaring only `ignores` keeps this rule set intact, including `default: false`
|
|
249
|
+
* and every per-rule option; but its `ignores` *replaces*
|
|
250
|
+
* {@link MARKDOWN_IGNORES} rather than extending it, so such a file must restate
|
|
251
|
+
* every shared entry it still wants. Omitting `CHANGELOG.md` there silently
|
|
252
|
+
* starts linting a generated file.
|
|
245
253
|
*
|
|
246
254
|
* @param {string} root - Repository to lint.
|
|
247
255
|
* @param {object} [opts]
|
package/engine/scenes.mjs
CHANGED
|
@@ -414,7 +414,7 @@ export class Scenes extends BasePackCompiler {
|
|
|
414
414
|
packageId: foundryPackageId(),
|
|
415
415
|
name,
|
|
416
416
|
folder,
|
|
417
|
-
stats:
|
|
417
|
+
stats: this.stats,
|
|
418
418
|
journalEntryId: entryId,
|
|
419
419
|
// A map note's prose is a derived JournalEntry: it lands in the
|
|
420
420
|
// default JournalEntry pack, not in whichever Scene pack the map
|
|
@@ -521,7 +521,7 @@ export class Scenes extends BasePackCompiler {
|
|
|
521
521
|
sort: 0,
|
|
522
522
|
flags: {},
|
|
523
523
|
_id: id,
|
|
524
|
-
_stats:
|
|
524
|
+
_stats: this.stats,
|
|
525
525
|
_key: `!adventures!${id}`,
|
|
526
526
|
};
|
|
527
527
|
}
|