@heroiclands/package-build 6.1.0 → 7.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 +614 -0
- package/CONTENT.md +21 -1
- package/bin/content-build.mjs +29 -10
- package/bin/package-build.mjs +8 -1
- package/config.mjs +25 -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/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/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/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/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/wikilink-syntax.d.mts +24 -0
- package/types/sohl/actors.d.mts +3 -3
package/bin/content-build.mjs
CHANGED
|
@@ -156,6 +156,25 @@ prefix.apply(log, {
|
|
|
156
156
|
},
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Report a command's failure.
|
|
161
|
+
*
|
|
162
|
+
* A configuration error carries its own `file:line:column: error: ` locator
|
|
163
|
+
* (#95), and `loglevel`'s `[timestamp] [ERROR]:` prefix occupies exactly the
|
|
164
|
+
* position a parser reads the path from — so a located failure is printed
|
|
165
|
+
* unprefixed, as `emitDiagnostic` prints every other finding. Everything else
|
|
166
|
+
* is ordinary prose and keeps the log line it always had.
|
|
167
|
+
*
|
|
168
|
+
* @param {unknown} err - What was thrown.
|
|
169
|
+
* @returns {void}
|
|
170
|
+
*/
|
|
171
|
+
function reportFailure(err) {
|
|
172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
173
|
+
if (/** @type {{located?: boolean}} */ (err)?.located)
|
|
174
|
+
console.error(message);
|
|
175
|
+
else log.error(message);
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
const argv = yargs(hideBin(process.argv))
|
|
160
179
|
.command(packageCommand())
|
|
161
180
|
.command(depsCommand())
|
|
@@ -303,7 +322,7 @@ function docsCommand() {
|
|
|
303
322
|
process.stdout.write(page);
|
|
304
323
|
}
|
|
305
324
|
} catch (err) {
|
|
306
|
-
|
|
325
|
+
reportFailure(err);
|
|
307
326
|
process.exitCode = 1;
|
|
308
327
|
}
|
|
309
328
|
},
|
|
@@ -385,7 +404,7 @@ function lintCommand() {
|
|
|
385
404
|
);
|
|
386
405
|
}
|
|
387
406
|
} catch (err) {
|
|
388
|
-
|
|
407
|
+
reportFailure(err);
|
|
389
408
|
process.exitCode = 1;
|
|
390
409
|
}
|
|
391
410
|
},
|
|
@@ -466,7 +485,7 @@ function formatCommand() {
|
|
|
466
485
|
log.info(`Formatting is clean (${checked} file(s)).`);
|
|
467
486
|
}
|
|
468
487
|
} catch (err) {
|
|
469
|
-
|
|
488
|
+
reportFailure(err);
|
|
470
489
|
process.exitCode = 1;
|
|
471
490
|
}
|
|
472
491
|
},
|
|
@@ -515,7 +534,7 @@ function markdownCommand() {
|
|
|
515
534
|
log.info("Markdown is clean.");
|
|
516
535
|
}
|
|
517
536
|
} catch (err) {
|
|
518
|
-
|
|
537
|
+
reportFailure(err);
|
|
519
538
|
process.exitCode = 1;
|
|
520
539
|
}
|
|
521
540
|
},
|
|
@@ -663,7 +682,7 @@ function linksCommand() {
|
|
|
663
682
|
);
|
|
664
683
|
}
|
|
665
684
|
} catch (err) {
|
|
666
|
-
|
|
685
|
+
reportFailure(err);
|
|
667
686
|
process.exitCode = 1;
|
|
668
687
|
}
|
|
669
688
|
},
|
|
@@ -739,7 +758,7 @@ function manifestCommand() {
|
|
|
739
758
|
});
|
|
740
759
|
}
|
|
741
760
|
} catch (err) {
|
|
742
|
-
|
|
761
|
+
reportFailure(err);
|
|
743
762
|
process.exitCode = 1;
|
|
744
763
|
}
|
|
745
764
|
},
|
|
@@ -868,7 +887,7 @@ function siteCommand() {
|
|
|
868
887
|
`landing(s) to ${path.relative(process.cwd(), s.out)}`,
|
|
869
888
|
);
|
|
870
889
|
} catch (err) {
|
|
871
|
-
|
|
890
|
+
reportFailure(err);
|
|
872
891
|
process.exitCode = 1;
|
|
873
892
|
}
|
|
874
893
|
},
|
|
@@ -962,7 +981,7 @@ function reachabilityCommand() {
|
|
|
962
981
|
);
|
|
963
982
|
}
|
|
964
983
|
} catch (err) {
|
|
965
|
-
|
|
984
|
+
reportFailure(err);
|
|
966
985
|
process.exitCode = 1;
|
|
967
986
|
}
|
|
968
987
|
},
|
|
@@ -1052,7 +1071,7 @@ function depsCommand() {
|
|
|
1052
1071
|
if (count)
|
|
1053
1072
|
log.info(`Fetched ${count} dependency catalogue(s).`);
|
|
1054
1073
|
} catch (err) {
|
|
1055
|
-
|
|
1074
|
+
reportFailure(err);
|
|
1056
1075
|
process.exitCode = 1;
|
|
1057
1076
|
}
|
|
1058
1077
|
},
|
|
@@ -1262,7 +1281,7 @@ function packageCommand() {
|
|
|
1262
1281
|
});
|
|
1263
1282
|
}
|
|
1264
1283
|
} catch (err) {
|
|
1265
|
-
|
|
1284
|
+
reportFailure(err);
|
|
1266
1285
|
process.exitCode = 1;
|
|
1267
1286
|
}
|
|
1268
1287
|
},
|
package/bin/package-build.mjs
CHANGED
|
@@ -135,7 +135,14 @@ function ownVersion() {
|
|
|
135
135
|
*/
|
|
136
136
|
function die(err) {
|
|
137
137
|
const message = err instanceof Error ? err.message : String(err);
|
|
138
|
-
|
|
138
|
+
// A located diagnostic already starts with `file:line:column:`, which is
|
|
139
|
+
// exactly the position a parser reads the path from — prefixing it would
|
|
140
|
+
// yield a filename no editor can open (#95).
|
|
141
|
+
console.error(
|
|
142
|
+
/** @type {{located?: boolean}} */ (err)?.located ? message : (
|
|
143
|
+
`package-build: ${message}`
|
|
144
|
+
),
|
|
145
|
+
);
|
|
139
146
|
process.exit(1);
|
|
140
147
|
}
|
|
141
148
|
|
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 = [
|
|
@@ -145,12 +149,21 @@ const ARTIFACT_OF_KIND = Object.freeze({
|
|
|
145
149
|
});
|
|
146
150
|
|
|
147
151
|
/**
|
|
152
|
+
* Reject a configured value, naming the key it was written under.
|
|
153
|
+
*
|
|
154
|
+
* The dotted path rides on the error as `field` as well as appearing in the
|
|
155
|
+
* message, so {@link loadPackageBuildConfig} — the half that knows which file
|
|
156
|
+
* was read — can resolve it to a line and column (#95). This half stays pure.
|
|
157
|
+
*
|
|
148
158
|
* @param {string} where - Dotted path of the offending key.
|
|
149
159
|
* @param {string} problem - What is wrong with it.
|
|
150
160
|
* @returns {never}
|
|
151
161
|
*/
|
|
152
162
|
function fail(where, problem) {
|
|
153
|
-
throw
|
|
163
|
+
throw Object.assign(
|
|
164
|
+
new TypeError(`package-build config: \`${where}\` ${problem}.`),
|
|
165
|
+
{ field: where },
|
|
166
|
+
);
|
|
154
167
|
}
|
|
155
168
|
|
|
156
169
|
/**
|
|
@@ -842,5 +855,14 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
842
855
|
* declares something malformed.
|
|
843
856
|
*/
|
|
844
857
|
export function loadPackageBuildConfig() {
|
|
845
|
-
|
|
858
|
+
const shared = loadPackConfig();
|
|
859
|
+
try {
|
|
860
|
+
return resolvePackageBuildConfig(shared);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
// The pure half names the offending key and nothing else; this half
|
|
863
|
+
// knows the file it was read from, so the position is attached here
|
|
864
|
+
// (#95) — the same boundary `configFromData` is for the rest of the
|
|
865
|
+
// configuration.
|
|
866
|
+
throw locateConfigError(err, packConfigPath());
|
|
867
|
+
}
|
|
846
868
|
}
|
package/content-config.mjs
CHANGED
|
@@ -584,9 +584,12 @@ const CONFIG_KEYS = [
|
|
|
584
584
|
"site",
|
|
585
585
|
"compatibility",
|
|
586
586
|
"relationships",
|
|
587
|
+
"systems",
|
|
588
|
+
"requiresSystem",
|
|
587
589
|
"packageBuild",
|
|
588
590
|
"publish",
|
|
589
591
|
];
|
|
592
|
+
const SYSTEM_KEYS = ["manifest", "compatibility"];
|
|
590
593
|
const COMPATIBILITY_KEYS = ["minimum", "verified"];
|
|
591
594
|
const DOCS_KEYS = ["itemFields"];
|
|
592
595
|
const SITE_KEYS = [
|
|
@@ -626,7 +629,24 @@ const PACK_KEYS = [
|
|
|
626
629
|
"system",
|
|
627
630
|
];
|
|
628
631
|
const PATH_KEYS = Object.keys(DEFAULT_PATHS);
|
|
629
|
-
const STATS_KEYS = ["
|
|
632
|
+
const STATS_KEYS = ["lastModifiedBy"];
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* How the loader hands {@link defineConfig} the system version it resolved.
|
|
636
|
+
*
|
|
637
|
+
* A **Symbol**, deliberately. `stats.systemVersion` is refused from an authored
|
|
638
|
+
* configuration (#48), but the value still has to reach here from the loader —
|
|
639
|
+
* which is the half that may do I/O, and which reads a system package's version
|
|
640
|
+
* out of the adjacent `package.json`. A string key would be a second spelling of
|
|
641
|
+
* the refused one, forgeable from YAML and reachable by `rejectUnknownKeys`; a
|
|
642
|
+
* symbol key cannot be written in YAML at all and does not appear in
|
|
643
|
+
* `Object.keys`, so the refusal has no back door.
|
|
644
|
+
*
|
|
645
|
+
* @type {symbol}
|
|
646
|
+
*/
|
|
647
|
+
export const DERIVED_SYSTEM_VERSION = Symbol.for(
|
|
648
|
+
"package-build.derivedSystemVersion",
|
|
649
|
+
);
|
|
630
650
|
const PUBLISH_KEYS = ["site", "manifests", "address"];
|
|
631
651
|
const MANIFEST_KEYS = ["publish", "consume"];
|
|
632
652
|
const ADDRESS_KEYS = ["prefix", "landing"];
|
|
@@ -637,12 +657,25 @@ function isPlainObject(value) {
|
|
|
637
657
|
}
|
|
638
658
|
|
|
639
659
|
/**
|
|
640
|
-
*
|
|
641
|
-
*
|
|
660
|
+
* Reject a configured value, naming the key it was written under.
|
|
661
|
+
*
|
|
662
|
+
* The dotted path is carried on the error as `field` as well as spelled into
|
|
663
|
+
* the message, because the message alone is a good description and a bad
|
|
664
|
+
* locator: the loader that read the file can resolve that path to a line and
|
|
665
|
+
* column, and does (`locateConfigError` in `engine/pack-config.mjs`, #95).
|
|
666
|
+
* Attaching it here rather than formatting here is what keeps this module
|
|
667
|
+
* free of I/O — it is the leaf an `.mjs` configuration imports, so it may not
|
|
668
|
+
* reach for the file it is validating.
|
|
669
|
+
*
|
|
670
|
+
* @param {string} field - Dotted path of the offending key.
|
|
671
|
+
* @param {string} problem - What is wrong with it.
|
|
642
672
|
* @returns {never}
|
|
643
673
|
*/
|
|
644
674
|
function fail(field, problem) {
|
|
645
|
-
throw
|
|
675
|
+
throw Object.assign(
|
|
676
|
+
new TypeError(`package-build config: \`${field}\` ${problem}.`),
|
|
677
|
+
{ field },
|
|
678
|
+
);
|
|
646
679
|
}
|
|
647
680
|
|
|
648
681
|
/**
|
|
@@ -869,28 +902,42 @@ function normalizePaths(value, rootDir) {
|
|
|
869
902
|
* @param {unknown} value
|
|
870
903
|
* @returns {Readonly<StatsSpec>}
|
|
871
904
|
*/
|
|
872
|
-
function normalizeStats(value) {
|
|
905
|
+
function normalizeStats(value, derived) {
|
|
873
906
|
if (!isPlainObject(value)) fail("stats", "must be an object");
|
|
874
907
|
const input = /** @type {Record<string, unknown>} */ (value);
|
|
908
|
+
|
|
909
|
+
// **`systemId` and `systemVersion` are derived, and authoring a derived
|
|
910
|
+
// value is an error rather than an override (#48).** `systems:` is the
|
|
911
|
+
// single source: it says which systems this package stamps against, and
|
|
912
|
+
// `requiresSystem` — or a lone declared system — says which one the
|
|
913
|
+
// package-wide block takes. A system package answers for itself.
|
|
914
|
+
//
|
|
915
|
+
// Refused rather than ignored, because the two would silently disagree.
|
|
916
|
+
// That is exactly how `stats.systemVersion` came to sit at `0.6.0` for four
|
|
917
|
+
// releases: a transcribed copy is free to drift from what it copied, and
|
|
918
|
+
// nothing reads a stamped `_stats` until something migrates on it.
|
|
919
|
+
for (const key of ["systemId", "systemVersion"]) {
|
|
920
|
+
if (input[key] === undefined) continue;
|
|
921
|
+
fail(
|
|
922
|
+
`stats.${key}`,
|
|
923
|
+
`is derived and may not be authored. ` +
|
|
924
|
+
(key === "systemId" ?
|
|
925
|
+
`A system package is its own system; a module takes it ` +
|
|
926
|
+
`from \`requiresSystem\`, or from \`systems:\` when it ` +
|
|
927
|
+
`declares exactly one. `
|
|
928
|
+
: `It is the \`compatibility.verified\` of the system in ` +
|
|
929
|
+
`\`systems:\`, or a system package's own \`package.json\` ` +
|
|
930
|
+
`version. `) +
|
|
931
|
+
`Remove the key`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
875
934
|
rejectUnknownKeys(input, STATS_KEYS, "stats.");
|
|
876
935
|
|
|
877
936
|
return Object.freeze({
|
|
878
|
-
//
|
|
879
|
-
// the
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
input.systemId === undefined || input.systemId === null ?
|
|
883
|
-
null
|
|
884
|
-
: requireNonEmptyString(input.systemId, "stats.systemId"),
|
|
885
|
-
// Optional for the same reason as `systemId`: a system-agnostic module
|
|
886
|
-
// is not built against a system, so it has no version of one to stamp.
|
|
887
|
-
systemVersion:
|
|
888
|
-
input.systemVersion === undefined || input.systemVersion === null ?
|
|
889
|
-
null
|
|
890
|
-
: requireNonEmptyString(
|
|
891
|
-
input.systemVersion,
|
|
892
|
-
"stats.systemVersion",
|
|
893
|
-
),
|
|
937
|
+
// Per pack where the packs differ — see `statsForPack` — and this is
|
|
938
|
+
// the package-wide answer for everything that has no pack in hand.
|
|
939
|
+
systemId: derived.systemId,
|
|
940
|
+
systemVersion: derived.systemVersion,
|
|
894
941
|
lastModifiedBy: requireNonEmptyString(
|
|
895
942
|
input.lastModifiedBy,
|
|
896
943
|
"stats.lastModifiedBy",
|
|
@@ -1184,6 +1231,98 @@ function normalizeCompatibility(value, where, requireMinimum = true) {
|
|
|
1184
1231
|
* @param {unknown} value - The `relationships` block, or `undefined`.
|
|
1185
1232
|
* @returns {Readonly<Relationships>} It, frozen; `{}` when absent.
|
|
1186
1233
|
*/
|
|
1234
|
+
/**
|
|
1235
|
+
* The systems this package can stamp content against — declaration only (#48).
|
|
1236
|
+
*
|
|
1237
|
+
* **Declaring is not requiring, and that separation is the whole point.** The
|
|
1238
|
+
* only place to state a system version used to be `relationships.systems`, and
|
|
1239
|
+
* that list is a *restriction*: Foundry's `supportsSystem` drops a module from
|
|
1240
|
+
* any world whose system it does not name. So a module shipping content for two
|
|
1241
|
+
* systems — `harn-ensemble` ships an HM3 pack, a SoHL pack and a system-neutral
|
|
1242
|
+
* journals pack — had to choose between naming its systems and remaining
|
|
1243
|
+
* loadable, and choosing the second meant stamping no system version at all on
|
|
1244
|
+
* content that certainly has one.
|
|
1245
|
+
*
|
|
1246
|
+
* Naming a system here restricts nothing. {@link normalizeRequiresSystem} is
|
|
1247
|
+
* what restricts, and it is separate and optional.
|
|
1248
|
+
*
|
|
1249
|
+
* Each entry carries the same `compatibility` shape a relationship does, and
|
|
1250
|
+
* `verified` is what a pack stamps: `_stats.systemVersion` records what the
|
|
1251
|
+
* content was *built against*, not the floor it tolerates.
|
|
1252
|
+
*
|
|
1253
|
+
* @param {unknown} value - The declared `systems:` mapping.
|
|
1254
|
+
* @returns {Readonly<Record<string, Readonly<object>>>} Frozen; `{}` when absent.
|
|
1255
|
+
*/
|
|
1256
|
+
function normalizeSystems(value) {
|
|
1257
|
+
if (value === undefined || value === null) return Object.freeze({});
|
|
1258
|
+
if (!isPlainObject(value))
|
|
1259
|
+
fail("systems", "must be a mapping of id to spec");
|
|
1260
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
1261
|
+
|
|
1262
|
+
const out = {};
|
|
1263
|
+
for (const [id, entry] of Object.entries(input)) {
|
|
1264
|
+
const at = `systems.${id}`;
|
|
1265
|
+
if (!id) fail("systems", "declares an empty system id");
|
|
1266
|
+
if (!isPlainObject(entry)) fail(at, "must be a mapping");
|
|
1267
|
+
const spec = /** @type {Record<string, unknown>} */ (entry);
|
|
1268
|
+
rejectUnknownKeys(spec, SYSTEM_KEYS, `${at}.`);
|
|
1269
|
+
|
|
1270
|
+
const compatibility = spec.compatibility;
|
|
1271
|
+
if (!isPlainObject(compatibility)) {
|
|
1272
|
+
fail(`${at}.compatibility`, "must be a mapping");
|
|
1273
|
+
}
|
|
1274
|
+
const compat = /** @type {Record<string, unknown>} */ (compatibility);
|
|
1275
|
+
rejectUnknownKeys(compat, COMPATIBILITY_KEYS, `${at}.compatibility.`);
|
|
1276
|
+
// `verified` is required because it is the value a pack stamps. A
|
|
1277
|
+
// declaration that cannot answer "which version was this built
|
|
1278
|
+
// against" is the gap this block exists to close.
|
|
1279
|
+
const verified = requireNonEmptyString(
|
|
1280
|
+
compat.verified,
|
|
1281
|
+
`${at}.compatibility.verified`,
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
out[id] = Object.freeze({
|
|
1285
|
+
manifest:
|
|
1286
|
+
spec.manifest === undefined || spec.manifest === null ?
|
|
1287
|
+
null
|
|
1288
|
+
: requireNonEmptyString(spec.manifest, `${at}.manifest`),
|
|
1289
|
+
compatibility: Object.freeze({
|
|
1290
|
+
minimum:
|
|
1291
|
+
compat.minimum === undefined || compat.minimum === null ?
|
|
1292
|
+
null
|
|
1293
|
+
: requireNonEmptyString(
|
|
1294
|
+
compat.minimum,
|
|
1295
|
+
`${at}.compatibility.minimum`,
|
|
1296
|
+
),
|
|
1297
|
+
verified,
|
|
1298
|
+
}),
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
return Object.freeze(out);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* The one system this package refuses to load without, or `null` (#48).
|
|
1306
|
+
*
|
|
1307
|
+
* The gate half of the split. Naming a system here emits
|
|
1308
|
+
* `relationships.systems` for it, which is what Foundry's `supportsSystem`
|
|
1309
|
+
* reads — so the package becomes unavailable under any other system. Omitted,
|
|
1310
|
+
* no relationship is emitted and the package loads anywhere, each pack stamping
|
|
1311
|
+
* whatever its own `system:` names.
|
|
1312
|
+
*
|
|
1313
|
+
* It reuses the {@link normalizeSystems} entry rather than restating the
|
|
1314
|
+
* compatibility: `stats.systemVersion` froze at `0.6.0` for four releases
|
|
1315
|
+
* because a transcription was free to disagree with what it copied, and a
|
|
1316
|
+
* second transcription invites the same.
|
|
1317
|
+
*
|
|
1318
|
+
* @param {unknown} value - The declared `requiresSystem:`.
|
|
1319
|
+
* @returns {string|null} The system id, or `null`.
|
|
1320
|
+
*/
|
|
1321
|
+
function normalizeRequiresSystem(value) {
|
|
1322
|
+
if (value === undefined || value === null) return null;
|
|
1323
|
+
return requireNonEmptyString(value, "requiresSystem");
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1187
1326
|
function normalizeRelationships(value) {
|
|
1188
1327
|
if (value === undefined) return Object.freeze({});
|
|
1189
1328
|
if (!isPlainObject(value)) fail("relationships", "must be a mapping");
|
|
@@ -1541,6 +1680,54 @@ export function defineConfig(config) {
|
|
|
1541
1680
|
seen.add(name);
|
|
1542
1681
|
}
|
|
1543
1682
|
|
|
1683
|
+
// ── systems: declaring, and requiring, are separate decisions (#48) ──────
|
|
1684
|
+
const systems = normalizeSystems(input.systems);
|
|
1685
|
+
const requiresSystem = normalizeRequiresSystem(input.requiresSystem);
|
|
1686
|
+
const declaredSystems = new Set(Object.keys(systems));
|
|
1687
|
+
/** `relationships.systems`, for the derivations that still consult it. */
|
|
1688
|
+
const relationshipSystems = /** @type {{id?: string}[]} */ (
|
|
1689
|
+
(isPlainObject(input.relationships) ?
|
|
1690
|
+
input.relationships.systems
|
|
1691
|
+
: null) ?? []
|
|
1692
|
+
);
|
|
1693
|
+
|
|
1694
|
+
// A name that resolves to nothing is a build error rather than a
|
|
1695
|
+
// fall-through, in the spirit the rest of this file already follows: a pack
|
|
1696
|
+
// stamping a system nobody declared would stamp `undefined`, which is the
|
|
1697
|
+
// plausible lie #43 was about.
|
|
1698
|
+
if (requiresSystem !== null && !declaredSystems.has(requiresSystem)) {
|
|
1699
|
+
fail(
|
|
1700
|
+
"requiresSystem",
|
|
1701
|
+
`names \`${requiresSystem}\`, which \`systems:\` does not declare` +
|
|
1702
|
+
(declaredSystems.size ?
|
|
1703
|
+
`. Declared: ${[...declaredSystems].join(", ")}`
|
|
1704
|
+
: ` — the \`systems:\` block is empty or absent`),
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
for (const pack of packs.flatMap((p) => [p, ...p.companions])) {
|
|
1708
|
+
if (!pack.system) continue;
|
|
1709
|
+
if (declaredSystems.size && !declaredSystems.has(pack.system)) {
|
|
1710
|
+
fail(
|
|
1711
|
+
`packs.${pack.name}.system`,
|
|
1712
|
+
`names \`${pack.system}\`, which \`systems:\` does not ` +
|
|
1713
|
+
`declare. Declared: ${[...declaredSystems].join(", ")}`,
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
// With a gate set, a pack for any other system could never be seen:
|
|
1717
|
+
// Foundry drops the whole package under a system `requiresSystem` does
|
|
1718
|
+
// not name, so the pack would ship and be unreachable.
|
|
1719
|
+
if (requiresSystem !== null && pack.system !== requiresSystem) {
|
|
1720
|
+
fail(
|
|
1721
|
+
`packs.${pack.name}.system`,
|
|
1722
|
+
`names \`${pack.system}\` while \`requiresSystem\` is ` +
|
|
1723
|
+
`\`${requiresSystem}\`, so this pack could never be seen — ` +
|
|
1724
|
+
`Foundry hides the whole package from any world whose ` +
|
|
1725
|
+
`system \`requiresSystem\` does not name. Drop ` +
|
|
1726
|
+
`\`requiresSystem\`, or correct the pack`,
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1544
1731
|
// Several packs of one document type are allowed — editorial grouping of
|
|
1545
1732
|
// same-type documents is ordinary Foundry practice, and collapsing such a
|
|
1546
1733
|
// layout breaks every stored compendium UUID (#1566). What is not allowed
|
|
@@ -1592,7 +1779,50 @@ export function defineConfig(config) {
|
|
|
1592
1779
|
// one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
|
|
1593
1780
|
assetRoot: `${packageKind}/${foundryPackage}/assets`,
|
|
1594
1781
|
paths: normalizePaths(input.paths, rootDir),
|
|
1595
|
-
|
|
1782
|
+
// The package-wide system, derived (#48). A **system** package is its
|
|
1783
|
+
// own system, which is true by construction and needs no declaration. A
|
|
1784
|
+
// **module** takes the one it requires, or the one system it declares
|
|
1785
|
+
// when there is exactly one; with several and no gate there is no
|
|
1786
|
+
// package-wide answer, and each pack carries its own.
|
|
1787
|
+
stats: normalizeStats(input.stats, {
|
|
1788
|
+
systemId:
|
|
1789
|
+
packageKind === "systems" ? foundryPackage
|
|
1790
|
+
: requiresSystem ? requiresSystem
|
|
1791
|
+
: Object.keys(systems).length === 1 ? Object.keys(systems)[0]
|
|
1792
|
+
// A lone `relationships.systems` entry is a declaration of
|
|
1793
|
+
// the system as much as a gate, so it still answers. That
|
|
1794
|
+
// matters because the relationship carries `itemCatalog`
|
|
1795
|
+
// too — a separate concern the split does not replace — so
|
|
1796
|
+
// a repository using it would otherwise have to restate its
|
|
1797
|
+
// compatibility under `systems:` purely to keep stamping,
|
|
1798
|
+
// which is the duplication this whole change exists to
|
|
1799
|
+
// remove. Several entries have no single answer and get
|
|
1800
|
+
// none.
|
|
1801
|
+
: relationshipSystems.length === 1 ?
|
|
1802
|
+
(relationshipSystems[0]?.id ?? null)
|
|
1803
|
+
: null,
|
|
1804
|
+
// Derived here where the answer is pure data — the `verified` of
|
|
1805
|
+
// whichever system the package-wide block takes — and supplied by
|
|
1806
|
+
// the loader otherwise. The loader is the half that may do I/O, and
|
|
1807
|
+
// the two cases needing it are a *system* package (its own
|
|
1808
|
+
// `package.json` version) and a module still deriving from
|
|
1809
|
+
// `relationships.systems`.
|
|
1810
|
+
systemVersion:
|
|
1811
|
+
(() => {
|
|
1812
|
+
const id =
|
|
1813
|
+
requiresSystem ??
|
|
1814
|
+
(Object.keys(systems).length === 1 ?
|
|
1815
|
+
Object.keys(systems)[0]
|
|
1816
|
+
: null);
|
|
1817
|
+
return id ?
|
|
1818
|
+
(systems[id]?.compatibility?.verified ?? null)
|
|
1819
|
+
: null;
|
|
1820
|
+
})() ??
|
|
1821
|
+
(isPlainObject(input.stats) ?
|
|
1822
|
+
input.stats[DERIVED_SYSTEM_VERSION]
|
|
1823
|
+
: null) ??
|
|
1824
|
+
null,
|
|
1825
|
+
}),
|
|
1596
1826
|
itemBuilders,
|
|
1597
1827
|
itemArt,
|
|
1598
1828
|
itemFields,
|
|
@@ -1616,6 +1846,8 @@ export function defineConfig(config) {
|
|
|
1616
1846
|
"compatibility",
|
|
1617
1847
|
),
|
|
1618
1848
|
relationships: normalizeRelationships(input.relationships),
|
|
1849
|
+
systems,
|
|
1850
|
+
requiresSystem,
|
|
1619
1851
|
packageBuild: normalizePackageBuild(input.packageBuild),
|
|
1620
1852
|
publish: normalizePublish(input.publish),
|
|
1621
1853
|
});
|
package/engine/base-compiler.mjs
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
convertNoteWikilinks,
|
|
74
74
|
collectContentDocs,
|
|
75
75
|
expandNoteTables,
|
|
76
|
+
statsForPack,
|
|
76
77
|
} from "./helpers.mjs";
|
|
77
78
|
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
78
79
|
import { assertNoDeclaredPackage } from "./note-package.mjs";
|
|
@@ -237,6 +238,7 @@ export class BasePackCompiler {
|
|
|
237
238
|
dest,
|
|
238
239
|
folderResolver = () => null,
|
|
239
240
|
packName,
|
|
241
|
+
packSystem = null,
|
|
240
242
|
docType,
|
|
241
243
|
router,
|
|
242
244
|
routingReporter = false,
|
|
@@ -262,11 +264,34 @@ export class BasePackCompiler {
|
|
|
262
264
|
writable: false,
|
|
263
265
|
});
|
|
264
266
|
this.packName = packName;
|
|
267
|
+
this.packSystem = packSystem;
|
|
265
268
|
this.docType = docType;
|
|
266
269
|
this.router = router;
|
|
267
270
|
this.routingReporter = routingReporter;
|
|
268
271
|
}
|
|
269
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The `_stats` block every entry this pass emits is stamped with (#48).
|
|
275
|
+
*
|
|
276
|
+
* Per pack rather than per package, because a module may ship the same
|
|
277
|
+
* content for two systems — `harn-ensemble` has an `actors-hm3` pack and an
|
|
278
|
+
* `actors-sohl` pack — and those documents were built against different
|
|
279
|
+
* system versions. A single global block stamped both identically.
|
|
280
|
+
*
|
|
281
|
+
* Memoised on the instance: one pass, one pack, one system, so the block is
|
|
282
|
+
* constant for the life of the compiler. The previous module-level memo
|
|
283
|
+
* could not be, because it was shared across passes for different packs.
|
|
284
|
+
*
|
|
285
|
+
* @returns {object} The block, built once per compiler.
|
|
286
|
+
*/
|
|
287
|
+
get stats() {
|
|
288
|
+
this.#stats ??= statsForPack(this.packSystem);
|
|
289
|
+
return this.#stats;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** @type {object|undefined} */
|
|
293
|
+
#stats;
|
|
294
|
+
|
|
270
295
|
/**
|
|
271
296
|
* Whether this pass's pack is the one a claimed note belongs in.
|
|
272
297
|
*
|