@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/CONTENT.md
CHANGED
|
@@ -155,7 +155,9 @@ the file sits in, fills the optional halves with their defaults
|
|
|
155
155
|
manifest switches off and `publish.site` at its `homepage` floor),
|
|
156
156
|
derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
|
|
157
157
|
freezes the result. A malformed configuration throws a `TypeError` naming the
|
|
158
|
-
offending field
|
|
158
|
+
offending field — and the line and column it was written on, in the
|
|
159
|
+
[located form](#diagnostics) — so it fails at load rather than as an empty pack
|
|
160
|
+
much later.
|
|
159
161
|
|
|
160
162
|
**Four values are derived rather than authored**, because each is something a
|
|
161
163
|
file can be asked for rather than told:
|
|
@@ -1160,6 +1162,24 @@ Two rules keep it that way, both in `engine/diagnostics.mjs`:
|
|
|
1160
1162
|
meaningless, `file: …` when only the note is known. Nothing defaults to
|
|
1161
1163
|
`1:1`, which would send a reader to the frontmatter every time.
|
|
1162
1164
|
|
|
1165
|
+
**A configuration error is located the same way.** Every check in
|
|
1166
|
+
`content-config.mjs` and `config.mjs` reports through one `fail()`, naming the
|
|
1167
|
+
offending key's dotted path — a good description and a bad locator, in a file
|
|
1168
|
+
that runs to hundreds of lines with sibling entries flow-mapped onto one. The
|
|
1169
|
+
path now rides on the error, and the loader that read the file resolves it
|
|
1170
|
+
against the YAML, so all of them come out located:
|
|
1171
|
+
|
|
1172
|
+
```text
|
|
1173
|
+
package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner).
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
The same two rules apply. A key the file never declares — a required one that is
|
|
1177
|
+
simply missing — has no node of its own, so the position names the **mapping it
|
|
1178
|
+
belongs in** and no further out; a missing _top-level_ key has nothing above it
|
|
1179
|
+
but the document, and an `.mjs` configuration has no YAML to resolve a path
|
|
1180
|
+
against at all. Both report `package-build.config.yaml: error: …`, the file
|
|
1181
|
+
without a line, rather than a line that would be wrong.
|
|
1182
|
+
|
|
1163
1183
|
Establishing a position at all takes three corrections, applied only where they
|
|
1164
1184
|
hold — see `positionInBody`. A body offset is not a file line until the
|
|
1165
1185
|
frontmatter's lines are added (`bodyLine`); the trim that strips the body can
|
package/bin/content-build.mjs
CHANGED
|
@@ -73,6 +73,12 @@ import {
|
|
|
73
73
|
import { renderItemFieldReference } from "../engine/field-reference.mjs";
|
|
74
74
|
import { lintContentTree } from "../engine/content-lint.mjs";
|
|
75
75
|
import { lintFrontmatter } from "../engine/frontmatter-lint.mjs";
|
|
76
|
+
import {
|
|
77
|
+
compareFields,
|
|
78
|
+
resolveSchemaArtifact,
|
|
79
|
+
undeclaredMessage,
|
|
80
|
+
unemittedMessage,
|
|
81
|
+
} from "../engine/schema-check.mjs";
|
|
76
82
|
// The one vocabulary, loaded whole. Every content project authors the full type
|
|
77
83
|
// set — an adventure module ships skills, beings and magic swords — so no
|
|
78
84
|
// consumer gets a subset (#19, #20).
|
|
@@ -156,6 +162,25 @@ prefix.apply(log, {
|
|
|
156
162
|
},
|
|
157
163
|
});
|
|
158
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Report a command's failure.
|
|
167
|
+
*
|
|
168
|
+
* A configuration error carries its own `file:line:column: error: ` locator
|
|
169
|
+
* (#95), and `loglevel`'s `[timestamp] [ERROR]:` prefix occupies exactly the
|
|
170
|
+
* position a parser reads the path from — so a located failure is printed
|
|
171
|
+
* unprefixed, as `emitDiagnostic` prints every other finding. Everything else
|
|
172
|
+
* is ordinary prose and keeps the log line it always had.
|
|
173
|
+
*
|
|
174
|
+
* @param {unknown} err - What was thrown.
|
|
175
|
+
* @returns {void}
|
|
176
|
+
*/
|
|
177
|
+
function reportFailure(err) {
|
|
178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
179
|
+
if (/** @type {{located?: boolean}} */ (err)?.located)
|
|
180
|
+
console.error(message);
|
|
181
|
+
else log.error(message);
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
const argv = yargs(hideBin(process.argv))
|
|
160
185
|
.command(packageCommand())
|
|
161
186
|
.command(depsCommand())
|
|
@@ -303,7 +328,7 @@ function docsCommand() {
|
|
|
303
328
|
process.stdout.write(page);
|
|
304
329
|
}
|
|
305
330
|
} catch (err) {
|
|
306
|
-
|
|
331
|
+
reportFailure(err);
|
|
307
332
|
process.exitCode = 1;
|
|
308
333
|
}
|
|
309
334
|
},
|
|
@@ -367,9 +392,79 @@ function lintCommand() {
|
|
|
367
392
|
references: argv.references,
|
|
368
393
|
});
|
|
369
394
|
|
|
395
|
+
// What the builders emit, against what the receiving system
|
|
396
|
+
// declares (#60). Reported here rather than at compile: it is
|
|
397
|
+
// a property of the *declarations*, not of any one note, so it
|
|
398
|
+
// is the same answer for every document and belongs where a
|
|
399
|
+
// reader is already being told about the vocabulary.
|
|
400
|
+
const schema = resolveSchemaArtifact(config);
|
|
401
|
+
const schemaFindings = [];
|
|
402
|
+
if (!schema && config.stats?.systemId) {
|
|
403
|
+
// Said out loud, because a check that quietly does nothing
|
|
404
|
+
// is indistinguishable from one that passed — and this
|
|
405
|
+
// whole issue exists because a defect went unnoticed for a
|
|
406
|
+
// release. A system before its first schema build, or a
|
|
407
|
+
// module pinning a version released before the artifact
|
|
408
|
+
// existed, lands here.
|
|
409
|
+
log.info(
|
|
410
|
+
`No published schema for ${config.stats.systemId}` +
|
|
411
|
+
`@${config.stats.systemVersion ?? "?"}, so emitted ` +
|
|
412
|
+
`\`system\` fields are unchecked. A system ` +
|
|
413
|
+
`generates its own; a module gets one from ` +
|
|
414
|
+
`\`content-build deps fetch\`.`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const fieldSpecs = config.itemFields ?? {};
|
|
418
|
+
const comparable = Object.keys(fieldSpecs).length > 0;
|
|
419
|
+
if (schema && !comparable) {
|
|
420
|
+
// A schema, and nothing to compare it against. The emitted
|
|
421
|
+
// side of this check is the `fields:` of `itemBuilders`, so
|
|
422
|
+
// a package whose compendium content is committed JSON
|
|
423
|
+
// rather than built from field declarations has an empty
|
|
424
|
+
// one — and every field the system declares would be
|
|
425
|
+
// reported as unemitted. That is hundreds of findings whose
|
|
426
|
+
// only content is that this package does not build
|
|
427
|
+
// documents this way, which is not news and not a defect.
|
|
428
|
+
//
|
|
429
|
+
// Said out loud rather than skipped in silence, for the
|
|
430
|
+
// same reason the absent-schema case is: a check that
|
|
431
|
+
// quietly does nothing reads exactly like one that passed.
|
|
432
|
+
log.info(
|
|
433
|
+
`Read ${config.stats?.systemId ?? "the system"}'s ` +
|
|
434
|
+
`schema, but this package declares no ` +
|
|
435
|
+
`\`itemBuilders\` field specifications — so there ` +
|
|
436
|
+
`is nothing to compare it against and the ` +
|
|
437
|
+
`emitted-versus-declared check does not apply.`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (schema && comparable) {
|
|
441
|
+
const { undeclared, unemitted } = compareFields({
|
|
442
|
+
builders: fieldSpecs,
|
|
443
|
+
artifact: schema.artifact,
|
|
444
|
+
});
|
|
445
|
+
for (const f of undeclared) {
|
|
446
|
+
schemaFindings.push({
|
|
447
|
+
file: schema.source,
|
|
448
|
+
severity: "error",
|
|
449
|
+
message: undeclaredMessage(f),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
for (const f of unemitted) {
|
|
453
|
+
// Advisory: a field the system fills at runtime, or one
|
|
454
|
+
// added ahead of the content that will use it, is not a
|
|
455
|
+
// defect.
|
|
456
|
+
emitDiagnostic({
|
|
457
|
+
file: schema.source,
|
|
458
|
+
severity: "warning",
|
|
459
|
+
message: unemittedMessage(f),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
370
464
|
const findings = [
|
|
371
465
|
...addresses.findings,
|
|
372
466
|
...frontmatter.findings,
|
|
467
|
+
...schemaFindings,
|
|
373
468
|
];
|
|
374
469
|
for (const finding of findings) emitDiagnostic(finding);
|
|
375
470
|
if (findings.length) {
|
|
@@ -385,7 +480,7 @@ function lintCommand() {
|
|
|
385
480
|
);
|
|
386
481
|
}
|
|
387
482
|
} catch (err) {
|
|
388
|
-
|
|
483
|
+
reportFailure(err);
|
|
389
484
|
process.exitCode = 1;
|
|
390
485
|
}
|
|
391
486
|
},
|
|
@@ -466,7 +561,7 @@ function formatCommand() {
|
|
|
466
561
|
log.info(`Formatting is clean (${checked} file(s)).`);
|
|
467
562
|
}
|
|
468
563
|
} catch (err) {
|
|
469
|
-
|
|
564
|
+
reportFailure(err);
|
|
470
565
|
process.exitCode = 1;
|
|
471
566
|
}
|
|
472
567
|
},
|
|
@@ -515,7 +610,7 @@ function markdownCommand() {
|
|
|
515
610
|
log.info("Markdown is clean.");
|
|
516
611
|
}
|
|
517
612
|
} catch (err) {
|
|
518
|
-
|
|
613
|
+
reportFailure(err);
|
|
519
614
|
process.exitCode = 1;
|
|
520
615
|
}
|
|
521
616
|
},
|
|
@@ -663,7 +758,7 @@ function linksCommand() {
|
|
|
663
758
|
);
|
|
664
759
|
}
|
|
665
760
|
} catch (err) {
|
|
666
|
-
|
|
761
|
+
reportFailure(err);
|
|
667
762
|
process.exitCode = 1;
|
|
668
763
|
}
|
|
669
764
|
},
|
|
@@ -739,7 +834,7 @@ function manifestCommand() {
|
|
|
739
834
|
});
|
|
740
835
|
}
|
|
741
836
|
} catch (err) {
|
|
742
|
-
|
|
837
|
+
reportFailure(err);
|
|
743
838
|
process.exitCode = 1;
|
|
744
839
|
}
|
|
745
840
|
},
|
|
@@ -868,7 +963,7 @@ function siteCommand() {
|
|
|
868
963
|
`landing(s) to ${path.relative(process.cwd(), s.out)}`,
|
|
869
964
|
);
|
|
870
965
|
} catch (err) {
|
|
871
|
-
|
|
966
|
+
reportFailure(err);
|
|
872
967
|
process.exitCode = 1;
|
|
873
968
|
}
|
|
874
969
|
},
|
|
@@ -962,7 +1057,7 @@ function reachabilityCommand() {
|
|
|
962
1057
|
);
|
|
963
1058
|
}
|
|
964
1059
|
} catch (err) {
|
|
965
|
-
|
|
1060
|
+
reportFailure(err);
|
|
966
1061
|
process.exitCode = 1;
|
|
967
1062
|
}
|
|
968
1063
|
},
|
|
@@ -1052,7 +1147,7 @@ function depsCommand() {
|
|
|
1052
1147
|
if (count)
|
|
1053
1148
|
log.info(`Fetched ${count} dependency catalogue(s).`);
|
|
1054
1149
|
} catch (err) {
|
|
1055
|
-
|
|
1150
|
+
reportFailure(err);
|
|
1056
1151
|
process.exitCode = 1;
|
|
1057
1152
|
}
|
|
1058
1153
|
},
|
|
@@ -1262,7 +1357,7 @@ function packageCommand() {
|
|
|
1262
1357
|
});
|
|
1263
1358
|
}
|
|
1264
1359
|
} catch (err) {
|
|
1265
|
-
|
|
1360
|
+
reportFailure(err);
|
|
1266
1361
|
process.exitCode = 1;
|
|
1267
1362
|
}
|
|
1268
1363
|
},
|
package/bin/package-build.mjs
CHANGED
|
@@ -80,6 +80,8 @@ import { hideBin } from "yargs/helpers";
|
|
|
80
80
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
81
81
|
import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
|
|
82
82
|
import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
|
|
83
|
+
import { buildSchemaArtifact } from "../engine/schema-extract.mjs";
|
|
84
|
+
import { SCHEMA_ARTIFACT_FILE } from "../engine/foreign-catalog.mjs";
|
|
83
85
|
import { validateLangSource } from "../lang.mjs";
|
|
84
86
|
import {
|
|
85
87
|
analyzeCoverage,
|
|
@@ -135,7 +137,14 @@ function ownVersion() {
|
|
|
135
137
|
*/
|
|
136
138
|
function die(err) {
|
|
137
139
|
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
-
|
|
140
|
+
// A located diagnostic already starts with `file:line:column:`, which is
|
|
141
|
+
// exactly the position a parser reads the path from — prefixing it would
|
|
142
|
+
// yield a filename no editor can open (#95).
|
|
143
|
+
console.error(
|
|
144
|
+
/** @type {{located?: boolean}} */ (err)?.located ? message : (
|
|
145
|
+
`package-build: ${message}`
|
|
146
|
+
),
|
|
147
|
+
);
|
|
139
148
|
process.exit(1);
|
|
140
149
|
}
|
|
141
150
|
|
|
@@ -305,6 +314,109 @@ function assetsCommand() {
|
|
|
305
314
|
};
|
|
306
315
|
}
|
|
307
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Format generated text the way the repository formats everything else.
|
|
319
|
+
*
|
|
320
|
+
* Not cosmetic. A generated file that Prettier would reformat leaves
|
|
321
|
+
* `lint:format` and the generator's own `--check` each demanding what the other
|
|
322
|
+
* forbids, and the repository cannot be made green. Resolving the config from
|
|
323
|
+
* the *output path* is what makes one implementation here serve repositories
|
|
324
|
+
* with different Prettier settings.
|
|
325
|
+
*
|
|
326
|
+
* Imported on use, as `prose-lint.mjs` does, so that commands which never
|
|
327
|
+
* format do not pay to load it.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} text - The unformatted content.
|
|
330
|
+
* @param {string} filepath - Where it will be written.
|
|
331
|
+
* @returns {Promise<string>} The formatted content.
|
|
332
|
+
*/
|
|
333
|
+
async function formatGenerated(text, filepath) {
|
|
334
|
+
const prettier = await import("prettier");
|
|
335
|
+
const config = await prettier.resolveConfig(filepath);
|
|
336
|
+
return prettier.format(text, { ...config, filepath });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* `schema` — publish this package's DataModel field sets as `schema.json`.
|
|
341
|
+
*
|
|
342
|
+
* The producing half of the check `content-build lint` runs: Foundry discards
|
|
343
|
+
* an unknown `system` key at construction and says nothing, so a content build
|
|
344
|
+
* needs to know what a document will actually receive (#60). It cannot ask a
|
|
345
|
+
* running Foundry, and it cannot read `defineSchema()` from a sibling checkout,
|
|
346
|
+
* so the system publishes the field sets as data — the same shape the link
|
|
347
|
+
* manifest already uses for addresses.
|
|
348
|
+
*
|
|
349
|
+
* `--check` fails when the committed copy disagrees with what the source would
|
|
350
|
+
* produce now, because a generated file nothing checks drifts from its
|
|
351
|
+
* generator silently — and this one is read by other repositories.
|
|
352
|
+
*
|
|
353
|
+
* @returns {object} The yargs command module.
|
|
354
|
+
*/
|
|
355
|
+
function schemaCommand() {
|
|
356
|
+
return {
|
|
357
|
+
command: "schema",
|
|
358
|
+
describe: "Publish this package's DataModel field sets as schema.json",
|
|
359
|
+
builder: (y) =>
|
|
360
|
+
y.option("check", {
|
|
361
|
+
type: "boolean",
|
|
362
|
+
default: false,
|
|
363
|
+
describe:
|
|
364
|
+
"Fail when the committed schema.json is out of date " +
|
|
365
|
+
"rather than rewriting it",
|
|
366
|
+
}),
|
|
367
|
+
handler: handler(async (argv) => {
|
|
368
|
+
const config = loadPackageBuildConfig();
|
|
369
|
+
if (!config.schema.length) {
|
|
370
|
+
console.log(
|
|
371
|
+
"package-build: no `packageBuild.schema` declared; " +
|
|
372
|
+
"nothing to publish.",
|
|
373
|
+
);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const pkg = readPackageJson(config);
|
|
378
|
+
const artifact = buildSchemaArtifact({
|
|
379
|
+
rootDir: config.rootDir,
|
|
380
|
+
registries: config.schema,
|
|
381
|
+
packageId: config.packageId,
|
|
382
|
+
version: pkg.version,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const out = path.join(config.rootDir, SCHEMA_ARTIFACT_FILE);
|
|
386
|
+
const text = await formatGenerated(JSON.stringify(artifact), out);
|
|
387
|
+
const counts = Object.entries(artifact.documents)
|
|
388
|
+
.map(
|
|
389
|
+
([kind, subtypes]) =>
|
|
390
|
+
`${Object.keys(subtypes).length} ${kind}`,
|
|
391
|
+
)
|
|
392
|
+
.join(", ");
|
|
393
|
+
|
|
394
|
+
if (argv.check) {
|
|
395
|
+
const current =
|
|
396
|
+
fs.existsSync(out) ? fs.readFileSync(out, "utf8") : null;
|
|
397
|
+
if (current !== text) {
|
|
398
|
+
die(
|
|
399
|
+
`${SCHEMA_ARTIFACT_FILE} does not match what this ` +
|
|
400
|
+
`package's data models would produce — regenerate ` +
|
|
401
|
+
`it with \`package-build schema\`.`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
console.log(
|
|
405
|
+
`✅ ${SCHEMA_ARTIFACT_FILE} is up to date ` +
|
|
406
|
+
`(${counts} subtypes).`,
|
|
407
|
+
);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
fs.writeFileSync(out, text, "utf8");
|
|
412
|
+
console.log(
|
|
413
|
+
`✅ Wrote ${SCHEMA_ARTIFACT_FILE} for ${artifact.system} ` +
|
|
414
|
+
`${artifact.systemVersion} (${counts} subtypes).`,
|
|
415
|
+
);
|
|
416
|
+
}),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
308
420
|
/**
|
|
309
421
|
* `manifest` — generate `system.json` / `module.json` into the build stage.
|
|
310
422
|
*
|
|
@@ -920,6 +1032,7 @@ yargs(hideBin(process.argv))
|
|
|
920
1032
|
.command(cleanCommand())
|
|
921
1033
|
.command(assetsCommand())
|
|
922
1034
|
.command(manifestCommand())
|
|
1035
|
+
.command(schemaCommand())
|
|
923
1036
|
.command(langCommand())
|
|
924
1037
|
.command(bundleCommand())
|
|
925
1038
|
.command(releaseCommand())
|
package/config.mjs
CHANGED
|
@@ -59,7 +59,11 @@
|
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
61
|
import path from "node:path";
|
|
62
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
loadPackConfig,
|
|
64
|
+
locateConfigError,
|
|
65
|
+
packConfigPath,
|
|
66
|
+
} from "./engine/pack-config.mjs";
|
|
63
67
|
|
|
64
68
|
/** Keys the reserved section may declare. */
|
|
65
69
|
const SECTION_KEYS = [
|
|
@@ -68,6 +72,7 @@ const SECTION_KEYS = [
|
|
|
68
72
|
"assetTransform",
|
|
69
73
|
"manifest",
|
|
70
74
|
"manifestFlags",
|
|
75
|
+
"schema",
|
|
71
76
|
"clean",
|
|
72
77
|
"lang",
|
|
73
78
|
"deploy",
|
|
@@ -145,12 +150,21 @@ const ARTIFACT_OF_KIND = Object.freeze({
|
|
|
145
150
|
});
|
|
146
151
|
|
|
147
152
|
/**
|
|
153
|
+
* Reject a configured value, naming the key it was written under.
|
|
154
|
+
*
|
|
155
|
+
* The dotted path rides on the error as `field` as well as appearing in the
|
|
156
|
+
* message, so {@link loadPackageBuildConfig} — the half that knows which file
|
|
157
|
+
* was read — can resolve it to a line and column (#95). This half stays pure.
|
|
158
|
+
*
|
|
148
159
|
* @param {string} where - Dotted path of the offending key.
|
|
149
160
|
* @param {string} problem - What is wrong with it.
|
|
150
161
|
* @returns {never}
|
|
151
162
|
*/
|
|
152
163
|
function fail(where, problem) {
|
|
153
|
-
throw
|
|
164
|
+
throw Object.assign(
|
|
165
|
+
new TypeError(`package-build config: \`${where}\` ${problem}.`),
|
|
166
|
+
{ field: where },
|
|
167
|
+
);
|
|
154
168
|
}
|
|
155
169
|
|
|
156
170
|
/**
|
|
@@ -214,6 +228,41 @@ function normalizeAsset(value, index) {
|
|
|
214
228
|
});
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Validate the DataModel registries `package-build schema` reads.
|
|
233
|
+
*
|
|
234
|
+
* Each entry names a file and the binding in it that maps subtype to DataModel
|
|
235
|
+
* class. Two keys rather than a convention because the two repositories that
|
|
236
|
+
* declare this disagree on both: one keeps `ITEM_DM_DEF` and `ACTOR_DM_DEF` in
|
|
237
|
+
* a single configuration module, the other keeps `itemModels` and `actorModels`
|
|
238
|
+
* in a file apiece. Neither layout is more correct, and guessing between them
|
|
239
|
+
* would fail by reading nothing rather than by complaining.
|
|
240
|
+
*
|
|
241
|
+
* The document type is the key, so a package that models only Items simply says
|
|
242
|
+
* so — there is no empty `Actor` entry to write.
|
|
243
|
+
*
|
|
244
|
+
* @param {unknown} value - The `schema` block, or `undefined`.
|
|
245
|
+
* @returns {Readonly<object[]>} Registry entries; empty when absent.
|
|
246
|
+
*/
|
|
247
|
+
function normalizeSchema(value) {
|
|
248
|
+
if (value === undefined) return Object.freeze([]);
|
|
249
|
+
if (!isMapping(value)) fail("packageBuild.schema", "must be a mapping");
|
|
250
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
251
|
+
|
|
252
|
+
const entries = Object.entries(input).map(([documentType, entry]) => {
|
|
253
|
+
const where = `packageBuild.schema.${documentType}`;
|
|
254
|
+
if (!isMapping(entry)) fail(where, "must be a mapping");
|
|
255
|
+
const row = /** @type {Record<string, unknown>} */ (entry);
|
|
256
|
+
rejectUnknownKeys(row, ["from", "registry"], `${where}.`);
|
|
257
|
+
return Object.freeze({
|
|
258
|
+
documentType,
|
|
259
|
+
from: requireNonEmptyString(row.from, `${where}.from`),
|
|
260
|
+
registry: requireNonEmptyString(row.registry, `${where}.registry`),
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
return Object.freeze(entries);
|
|
264
|
+
}
|
|
265
|
+
|
|
217
266
|
/**
|
|
218
267
|
* Validate the manifest specification.
|
|
219
268
|
*
|
|
@@ -682,6 +731,7 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
682
731
|
"packageBuild.release.artifact",
|
|
683
732
|
),
|
|
684
733
|
assets: Object.freeze(assets),
|
|
734
|
+
schema: normalizeSchema(section.schema),
|
|
685
735
|
assetTransform:
|
|
686
736
|
section.assetTransform === undefined ?
|
|
687
737
|
null
|
|
@@ -842,5 +892,14 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
842
892
|
* declares something malformed.
|
|
843
893
|
*/
|
|
844
894
|
export function loadPackageBuildConfig() {
|
|
845
|
-
|
|
895
|
+
const shared = loadPackConfig();
|
|
896
|
+
try {
|
|
897
|
+
return resolvePackageBuildConfig(shared);
|
|
898
|
+
} catch (err) {
|
|
899
|
+
// The pure half names the offending key and nothing else; this half
|
|
900
|
+
// knows the file it was read from, so the position is attached here
|
|
901
|
+
// (#95) — the same boundary `configFromData` is for the rest of the
|
|
902
|
+
// configuration.
|
|
903
|
+
throw locateConfigError(err, packConfigPath());
|
|
904
|
+
}
|
|
846
905
|
}
|