@heroiclands/package-build 7.0.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 +226 -0
- package/bin/content-build.mjs +76 -0
- package/bin/package-build.mjs +106 -0
- package/config.mjs +37 -0
- package/engine/foreign-catalog.mjs +47 -0
- package/engine/schema-check.mjs +332 -0
- package/engine/schema-extract.mjs +611 -0
- package/package.json +1 -1
- package/sohl/item-fields.mjs +0 -35
- package/types/engine/foreign-catalog.d.mts +15 -0
- package/types/engine/schema-check.d.mts +176 -0
- package/types/engine/schema-extract.d.mts +61 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,231 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 8.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- d324b5a: Stop emitting five `system` fields no SoHL DataModel declares (#60).
|
|
8
|
+
|
|
9
|
+
The comparison this release adds was run against sohl's published schema at
|
|
10
|
+
0.8.2 and found five, on its first run:
|
|
11
|
+
|
|
12
|
+
| type | emitted, undeclared |
|
|
13
|
+
| ---------------- | -------------------------------------------------------- |
|
|
14
|
+
| `affliction` | `isTreated` |
|
|
15
|
+
| `trauma` | `isTreated`, `isBleeding` |
|
|
16
|
+
| `projectilegear` | `impactBase.overrideDice`, `impactBase.overrideModifier` |
|
|
17
|
+
|
|
18
|
+
Every one was discarded when the document was constructed, on every compiled
|
|
19
|
+
document, with nothing said — which is the whole of what #60 is about.
|
|
20
|
+
|
|
21
|
+
**Two of them were never storable.** `isTreated` and `isBleeding` are _derived_
|
|
22
|
+
on the logic classes: `AfflictionLogic.isTreated` is `treatmentDate != null`,
|
|
23
|
+
and `TraumaLogic.isBleeding` is `bloodLossAdvanceDurationBase != null`. So the
|
|
24
|
+
builder wrote a constant Foundry threw away while the field it is computed from
|
|
25
|
+
went unwritten — both directions of the same defect, on the same field. Nothing
|
|
26
|
+
replaces them: an untreated affliction is one whose `treatmentDate` is unset,
|
|
27
|
+
which is already the initial value.
|
|
28
|
+
|
|
29
|
+
**Three were authored fields that vanished.** `trauma.isTreated`,
|
|
30
|
+
`trauma.isBleeding` and the two projectile overrides carried a frontmatter
|
|
31
|
+
`name`, so a note could write them — and the value went nowhere.
|
|
32
|
+
|
|
33
|
+
**The projectile overrides are removed rather than reported upstream as missing
|
|
34
|
+
fields**, because nothing anywhere wants them: no DataModel declares them, no
|
|
35
|
+
logic class reads them, no localization key names them. A
|
|
36
|
+
launcher-versus-ammunition override may be worth having, but it would have to be
|
|
37
|
+
designed in the system first, and a content builder cannot be where it is
|
|
38
|
+
invented.
|
|
39
|
+
|
|
40
|
+
**Checked before removing, because three were authored.** Dropping an authored
|
|
41
|
+
field turns a note that writes it from a silent loss into an unknown-key error,
|
|
42
|
+
so all six content trees were searched first: `sohl`, `thalorna`, `kethira`,
|
|
43
|
+
`harnensemble`, `harnadventures` and `hm3` write none of the five.
|
|
44
|
+
|
|
45
|
+
**Verified.** Against sohl 0.8.2's published schema, `undeclared` falls from
|
|
46
|
+
five to zero. The twelve remaining findings are the advisory direction —
|
|
47
|
+
fields a subtype declares that no builder emits, `treatmentDate` among them —
|
|
48
|
+
and are reported rather than fatal.
|
|
49
|
+
|
|
50
|
+
**Bump**
|
|
51
|
+
|
|
52
|
+
_Major._ Three of the five were part of the authored frontmatter vocabulary, and
|
|
53
|
+
a note writing one is now an unknown-key error rather than a value silently
|
|
54
|
+
dropped. No content in this organisation writes them, but a consumer outside it
|
|
55
|
+
would have to delete the keys — and would find its documents unchanged, since
|
|
56
|
+
they never reached a saved document in the first place.
|
|
57
|
+
|
|
58
|
+
### Minor Changes
|
|
59
|
+
|
|
60
|
+
- d324b5a: Run the emitted-versus-declared field comparison in `content-build lint` (#60).
|
|
61
|
+
|
|
62
|
+
The comparison shipped in 7.0.0 with nothing calling it, because no system had
|
|
63
|
+
published its field sets yet. `sohl` now does, so this reads the artifact and
|
|
64
|
+
runs both directions.
|
|
65
|
+
|
|
66
|
+
**Which system, at which version, is already settled.** `stats.systemId` and
|
|
67
|
+
`stats.systemVersion` are derived rather than authored (#48) — a system package
|
|
68
|
+
is its own system, a module takes the one it requires, and the version is the
|
|
69
|
+
`compatibility.verified` it pins. So there is no second piece of configuration
|
|
70
|
+
to disagree with the first about whose schema to check against.
|
|
71
|
+
|
|
72
|
+
Two places to find it:
|
|
73
|
+
|
|
74
|
+
- **A system** reads its own `schema.json`, generated from its `src/`.
|
|
75
|
+
- **A module** reads the copy `content-build deps fetch` caches from the archive
|
|
76
|
+
of the version it pins — which is what makes the comparison happen at
|
|
77
|
+
`verified` rather than against whatever the system's `main` holds today.
|
|
78
|
+
|
|
79
|
+
**The fetch now keeps the schema.** Both fetch paths already unpacked the
|
|
80
|
+
dependency's archive, but only one kept the result: a download unzips into
|
|
81
|
+
`<cache>/package/` and leaves it, while `deps fetch --from` unzips into a
|
|
82
|
+
temporary directory and deletes it. A reader looking in the unpacked tree would
|
|
83
|
+
have found the schema for one and not the other — so it is copied to one known
|
|
84
|
+
place beside the extracted items instead.
|
|
85
|
+
|
|
86
|
+
**An absent schema is announced, not skipped in silence.** A system before its
|
|
87
|
+
first schema build, and a module pinning a version released before the artifact
|
|
88
|
+
existed, both have nothing to check against. That is not an error — but a check
|
|
89
|
+
that quietly does nothing is indistinguishable from one that passed, and this
|
|
90
|
+
issue exists because a defect went unnoticed for a release. So the run says so:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
No published schema for sohl@0.8.2, so emitted `system` fields are unchecked.
|
|
94
|
+
A system generates its own; a module gets one from `content-build deps fetch`.
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Also lands the five fixes the comparison found.** They were pushed after
|
|
98
|
+
#116's merge and so were not part of it: `affliction` and `trauma` emitted
|
|
99
|
+
`isTreated`, `trauma` emitted `isBleeding`, and `projectilegear` emitted
|
|
100
|
+
`impactBase.overrideDice` and `impactBase.overrideModifier` — none of which any
|
|
101
|
+
DataModel declares. Without them this change would have turned `sohl`'s own
|
|
102
|
+
lint red on the defects it was written to find.
|
|
103
|
+
|
|
104
|
+
**Verified against both shapes.** Against `sohl`, the run reports zero errors
|
|
105
|
+
and twelve advisory warnings, and `lint` passes. Against `sohl-kethira-basic`,
|
|
106
|
+
whose pinned 0.8.2 archive predates the artifact, it announces the skip and
|
|
107
|
+
reports only that repository's pre-existing findings.
|
|
108
|
+
|
|
109
|
+
**Bump**
|
|
110
|
+
|
|
111
|
+
_Minor._ New reporting on an existing command, and a fetch that keeps one more
|
|
112
|
+
file. The error direction can fail a build that passed before — but only for a
|
|
113
|
+
package whose dependency publishes a schema, which no released version does yet.
|
|
114
|
+
- ee9a9a8: Compare a builder's emitted `system` fields against the receiving DataModel
|
|
115
|
+
(#60) — the comparison half.
|
|
116
|
+
|
|
117
|
+
Foundry discards an unknown `system` key when a document is constructed, and
|
|
118
|
+
says nothing: the value is absent at load while the build that wrote it reported
|
|
119
|
+
success. Both directions of that mismatch have already happened here, both
|
|
120
|
+
compiled clean, and both were found by set-subtracting compiled documents'
|
|
121
|
+
`system` keys against `defineSchema()` **by hand**.
|
|
122
|
+
|
|
123
|
+
**The emitted half needs neither compilation nor parsing.** `field-spec.mjs`
|
|
124
|
+
already makes the field list the only statement of the mapping — "the
|
|
125
|
+
declaration is the builder" — so every `system` path a type can emit is
|
|
126
|
+
`field.to`, known statically. Nothing compiles a document to find out.
|
|
127
|
+
|
|
128
|
+
**The declared half arrives as data, pinned to the declared version.** A system
|
|
129
|
+
publishes its field sets as an artifact and this reads it, the shape the link
|
|
130
|
+
manifest already uses for addresses. Against `compatibility.verified`, never the
|
|
131
|
+
system's `main`: `affiliation.subType` _is_ defined on sohl `main` and simply
|
|
132
|
+
unreleased, while `sohl-kethira-basic` pins `0.8.2` — so a check against `main`
|
|
133
|
+
passes and the field still evaporates for all 21 of its deities.
|
|
134
|
+
|
|
135
|
+
**`own` and `inherited` are recorded apart, and the two directions read
|
|
136
|
+
different sets.** A subtype's schema spreads its parent's, so `notes`, `docHtml`
|
|
137
|
+
and the rest land on every subtype; they are the system's own runtime concerns
|
|
138
|
+
and no content builder is expected to emit them.
|
|
139
|
+
|
|
140
|
+
| direction | read against | severity |
|
|
141
|
+
| --------------------- | ------------------------------------------------------------------ | -------- |
|
|
142
|
+
| emitted, not declared | `own` ∪ `inherited` — the field must exist somewhere | error |
|
|
143
|
+
| declared, not emitted | `own` only — what the subtype adds is what its builder answers for | report |
|
|
144
|
+
|
|
145
|
+
Collapsing them would report every inherited field on every type: a wall of
|
|
146
|
+
findings that are all correct and none actionable.
|
|
147
|
+
|
|
148
|
+
**A false positive the real schema caught before this shipped.** Run against
|
|
149
|
+
sohl's actual `mysticalability`, the _declared, not emitted_ direction reported
|
|
150
|
+
`charges.value` and `charges.max` on a type that populates them correctly — the
|
|
151
|
+
builder writes `charges` as a whole object and never names the leaves beneath
|
|
152
|
+
it. A declared path is now covered when the builder emits any ancestor. The
|
|
153
|
+
first real schema tried produced two false findings, which is exactly the kind
|
|
154
|
+
that teaches people to ignore a report.
|
|
155
|
+
|
|
156
|
+
**Verified against the real declarations.** With sohl's `mysticalability`
|
|
157
|
+
schema transcribed from source, the comparison reports nothing; with #35's
|
|
158
|
+
`assocMysteryCode` reinstated, it reports exactly that field.
|
|
159
|
+
|
|
160
|
+
**What this does not do yet.** It does not read an artifact from disk, and
|
|
161
|
+
nothing runs it in a build — those wait on a system actually publishing its
|
|
162
|
+
schemas, which is sohl's half and a separate change. The comparison, the format
|
|
163
|
+
and both regression cases are pinned here so that half has something to satisfy.
|
|
164
|
+
|
|
165
|
+
**Bump**
|
|
166
|
+
|
|
167
|
+
_Minor._ New surface — `engine/schema-check.mjs` and its exports — and nothing
|
|
168
|
+
existing changes behaviour. No consumer runs the comparison until an artifact
|
|
169
|
+
exists to run it against.
|
|
170
|
+
- 14cd092: Add `package-build schema`, so a system publishes its DataModel field sets from
|
|
171
|
+
here rather than from its own copy of an extractor.
|
|
172
|
+
|
|
173
|
+
The consuming half of this contract shipped in 7.0.0: `content-build lint`
|
|
174
|
+
subtracts what a package's builders emit from what a document will actually
|
|
175
|
+
receive, because Foundry discards an unknown `system` key at construction and
|
|
176
|
+
says nothing about it. The producing half lived in the first system that needed
|
|
177
|
+
it, which meant the second system to need it would have copied 491 lines — and,
|
|
178
|
+
worse, would have copied a hardcoded `SCHEMA_ARTIFACT_VERSION`, a constant this
|
|
179
|
+
package owns. Two producers stamping a third repository's constant by hand is
|
|
180
|
+
the drift worth removing before it happens rather than after: the version is now
|
|
181
|
+
imported by the producer, not restated.
|
|
182
|
+
|
|
183
|
+
**Why here and not in each system.** A DataModel's schema is only introspectable
|
|
184
|
+
inside Foundry — `defineSchema()` returns field classes that do not exist in
|
|
185
|
+
Node — so the field sets have to be read out of the source as an AST.
|
|
186
|
+
TypeScript's parser reads plain JavaScript too, and this package already pins
|
|
187
|
+
that compiler for `coverage.mjs`. Putting the reader here means a
|
|
188
|
+
JavaScript-only system does not acquire a TypeScript pin merely to describe its
|
|
189
|
+
own data models.
|
|
190
|
+
|
|
191
|
+
**Declared, because the two layouts in use disagree.**
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
packageBuild:
|
|
195
|
+
schema:
|
|
196
|
+
Item: { from: module/data/item-models.js, registry: itemModels }
|
|
197
|
+
Actor: { from: module/data/actor-models.js, registry: actorModels }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
One system keeps both registries in a single configuration module; the other
|
|
201
|
+
keeps one per file. Neither layout is more correct, and a convention guessing
|
|
202
|
+
between them would fail by reading _nothing_ rather than by complaining — which
|
|
203
|
+
is the worst failure available here, since an empty schema passes every check.
|
|
204
|
+
A registry that maps nothing is refused for the same reason.
|
|
205
|
+
|
|
206
|
+
**Four spellings of inheritance, all followed.** `...Super.defineSchema()`,
|
|
207
|
+
`...super.defineSchema()`, `Object.assign(super.defineSchema(), {…})`, and a
|
|
208
|
+
subclass with no `defineSchema()` at all. The last is a real and complete
|
|
209
|
+
declaration — `class MiscGearModel extends GearModel {}` — and reading it as
|
|
210
|
+
"declares nothing" would make every field of a whole subtype look undeclared.
|
|
211
|
+
`SchemaField` nesting is recorded as dotted paths whether written bare or as
|
|
212
|
+
`fields.SchemaField`, since both spellings are in use.
|
|
213
|
+
|
|
214
|
+
**A schema with nothing to compare against now says so.** The emitted side of
|
|
215
|
+
the check is the `fields:` of `itemBuilders`, so a package whose compendium
|
|
216
|
+
content is committed JSON rather than built from field declarations has an empty
|
|
217
|
+
one — and every field the system declares would have been reported as unemitted.
|
|
218
|
+
That is hundreds of findings whose only content is that the package does not
|
|
219
|
+
build documents that way, which is not news and not a defect. It is announced
|
|
220
|
+
once instead, for the same reason the absent-schema case is: a check that quietly
|
|
221
|
+
does nothing reads exactly like one that passed. The moment a builder declares
|
|
222
|
+
`fields:`, the comparison starts running on its own.
|
|
223
|
+
|
|
224
|
+
**Bump**
|
|
225
|
+
|
|
226
|
+
_Minor._ A new command, a new optional configuration key, and a `lint` that
|
|
227
|
+
reports strictly less than before.
|
|
228
|
+
|
|
3
229
|
## 7.0.0
|
|
4
230
|
|
|
5
231
|
### Major Changes
|
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).
|
|
@@ -386,9 +392,79 @@ function lintCommand() {
|
|
|
386
392
|
references: argv.references,
|
|
387
393
|
});
|
|
388
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
|
+
|
|
389
464
|
const findings = [
|
|
390
465
|
...addresses.findings,
|
|
391
466
|
...frontmatter.findings,
|
|
467
|
+
...schemaFindings,
|
|
392
468
|
];
|
|
393
469
|
for (const finding of findings) emitDiagnostic(finding);
|
|
394
470
|
if (findings.length) {
|
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,
|
|
@@ -312,6 +314,109 @@ function assetsCommand() {
|
|
|
312
314
|
};
|
|
313
315
|
}
|
|
314
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
|
+
|
|
315
420
|
/**
|
|
316
421
|
* `manifest` — generate `system.json` / `module.json` into the build stage.
|
|
317
422
|
*
|
|
@@ -927,6 +1032,7 @@ yargs(hideBin(process.argv))
|
|
|
927
1032
|
.command(cleanCommand())
|
|
928
1033
|
.command(assetsCommand())
|
|
929
1034
|
.command(manifestCommand())
|
|
1035
|
+
.command(schemaCommand())
|
|
930
1036
|
.command(langCommand())
|
|
931
1037
|
.command(bundleCommand())
|
|
932
1038
|
.command(releaseCommand())
|
package/config.mjs
CHANGED
|
@@ -72,6 +72,7 @@ const SECTION_KEYS = [
|
|
|
72
72
|
"assetTransform",
|
|
73
73
|
"manifest",
|
|
74
74
|
"manifestFlags",
|
|
75
|
+
"schema",
|
|
75
76
|
"clean",
|
|
76
77
|
"lang",
|
|
77
78
|
"deploy",
|
|
@@ -227,6 +228,41 @@ function normalizeAsset(value, index) {
|
|
|
227
228
|
});
|
|
228
229
|
}
|
|
229
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
|
+
|
|
230
266
|
/**
|
|
231
267
|
* Validate the manifest specification.
|
|
232
268
|
*
|
|
@@ -695,6 +731,7 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
695
731
|
"packageBuild.release.artifact",
|
|
696
732
|
),
|
|
697
733
|
assets: Object.freeze(assets),
|
|
734
|
+
schema: normalizeSchema(section.schema),
|
|
698
735
|
assetTransform:
|
|
699
736
|
section.assetTransform === undefined ?
|
|
700
737
|
null
|
|
@@ -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 {
|