@jbrowse/cli 4.3.0 → 5.0.0-beta.10

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.
Files changed (71) hide show
  1. package/README.md +451 -72
  2. package/dist/base.js +0 -3
  3. package/dist/bin.js +2 -1
  4. package/dist/cliFetch.js +3 -0
  5. package/dist/commands/add-assembly/index.js +46 -19
  6. package/dist/commands/add-assembly/utils.js +204 -290
  7. package/dist/commands/add-connection.js +73 -79
  8. package/dist/commands/add-track-json.js +24 -12
  9. package/dist/commands/add-track-utils/adapter-utils.js +189 -288
  10. package/dist/commands/add-track-utils/file-operations.js +27 -11
  11. package/dist/commands/add-track-utils/multiwig.js +47 -0
  12. package/dist/commands/add-track-utils/track-config.js +66 -25
  13. package/dist/commands/add-track-utils/validators.js +36 -20
  14. package/dist/commands/add-track.js +200 -62
  15. package/dist/commands/admin-server/index.js +39 -50
  16. package/dist/commands/admin-server/utils.js +238 -126
  17. package/dist/commands/create.js +18 -34
  18. package/dist/commands/make-density/density-generator.js +180 -0
  19. package/dist/commands/make-density/index.js +139 -0
  20. package/dist/commands/make-pif/index.js +133 -11
  21. package/dist/commands/make-pif/pif-generator.js +243 -43
  22. package/dist/commands/process-utils.js +13 -2
  23. package/dist/commands/remove-track.js +20 -13
  24. package/dist/commands/set-default-session.js +35 -31
  25. package/dist/commands/shared/config-operations.js +3 -10
  26. package/dist/commands/shared/density.js +12 -0
  27. package/dist/commands/shared/sort-utils.js +67 -10
  28. package/dist/commands/shared/validators.js +38 -3
  29. package/dist/commands/sort-bed.js +2 -33
  30. package/dist/commands/sort-gff.js +2 -33
  31. package/dist/commands/text-index/adapter-utils.js +19 -26
  32. package/dist/commands/text-index/aggregate.js +40 -34
  33. package/dist/commands/text-index/command.js +37 -7
  34. package/dist/commands/text-index/config-utils.js +62 -85
  35. package/dist/commands/text-index/file-list.js +32 -18
  36. package/dist/commands/text-index/index.js +4 -4
  37. package/dist/commands/text-index/indexing-utils.js +44 -96
  38. package/dist/commands/text-index/per-track.js +44 -31
  39. package/dist/commands/text-index/validators.js +1 -1
  40. package/dist/commands/upgrade.js +20 -32
  41. package/dist/commands/validate/configManifest.generated.js +7613 -0
  42. package/dist/commands/validate/configSchema.generated.js +20400 -0
  43. package/dist/commands/validate/displayDefaultKeys.js +28 -0
  44. package/dist/commands/validate/index.js +113 -0
  45. package/dist/commands/validate/liftConfig.js +34 -0
  46. package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
  47. package/dist/commands/validate/markRules/autoBin.js +27 -0
  48. package/dist/commands/validate/markRules/colorScale.js +244 -0
  49. package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
  50. package/dist/commands/validate/markRules/jexlStrings.js +23 -0
  51. package/dist/commands/validate/markRules/markProblems.js +532 -0
  52. package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
  53. package/dist/commands/validate/markRules/markSpecs.js +96 -0
  54. package/dist/commands/validate/markRules/markVocabulary.js +57 -0
  55. package/dist/commands/validate/markRules/shapeNameList.js +7 -0
  56. package/dist/commands/validate/markRules/stepChannels.js +55 -0
  57. package/dist/commands/validate/schemaValidate.js +425 -0
  58. package/dist/commands/validate/suggest.js +35 -0
  59. package/dist/commands/validate/types.js +6 -0
  60. package/dist/commands/validate/validateConfig.js +502 -0
  61. package/dist/index.js +104 -50
  62. package/dist/types/common.js +3 -27
  63. package/dist/utils.js +309 -128
  64. package/dist/version.js +1 -1
  65. package/package.json +8 -14
  66. package/bin/run +0 -4
  67. package/bundle/index.js +0 -3686
  68. package/dist/commands/make-pif/cigar-utils.js +0 -24
  69. package/dist/commands/make-pif/file-utils.js +0 -30
  70. package/dist/commands/track-utils.js +0 -74
  71. package/dist/util.js +0 -35
@@ -0,0 +1,502 @@
1
+ // Checks a JBrowse config (or a saved session file): the generated JSON Schema
2
+ // first, which reports every key or value the schema can judge, then the
3
+ // cross-references a schema cannot express and the warnings for stale
4
+ // spellings a migration still rewrites.
5
+ //
6
+ // Why a validator at all, given MST type-checks a config on load: MST models
7
+ // ignore snapshot keys they do not declare, so a misspelt slot leaves the track
8
+ // loading normally with the setting doing nothing.
9
+ //
10
+ // Pure: no filesystem, no process exit. The command wrapper owns both.
11
+ import { configManifest } from './configManifest.generated.js';
12
+ import { displayDefaultsForTrackType } from './displayDefaultKeys.js';
13
+ import { isRecord, liftToSnapshot } from './liftConfig.js';
14
+ import { colorProblems, scaleEndProblems } from './markRules/colorScale.js';
15
+ import { markProblems } from './markRules/markProblems.js';
16
+ import { hasDeclaredShape, rootConfigurationSlots, schemaProblems, } from './schemaValidate.js';
17
+ import { didYouMean } from './suggest.js';
18
+ class Report {
19
+ problems = [];
20
+ notes = [];
21
+ error(where, message) {
22
+ this.problems.push({ level: 'error', where, message });
23
+ }
24
+ warn(where, message) {
25
+ this.problems.push({ level: 'warning', where, message });
26
+ }
27
+ note(message) {
28
+ this.notes.push(message);
29
+ }
30
+ result() {
31
+ return {
32
+ problems: this.problems,
33
+ notes: this.notes,
34
+ errorCount: this.problems.filter(p => p.level === 'error').length,
35
+ warningCount: this.problems.filter(p => p.level === 'warning').length,
36
+ };
37
+ }
38
+ }
39
+ // A declared `sequenceAdapter` works and is simply the wrong way round: every
40
+ // feature adapter takes its sequence from the assembly the track is displayed
41
+ // against, so a hand-written one pins the track to that source. An undeclared
42
+ // one is the schema's unknown-key error, with its own message there.
43
+ const SELF_SUPPLIED = {
44
+ sequenceAdapter: '`sequenceAdapter` is set by hand. JBrowse takes the sequence from the assembly the track is displayed against, so this is only needed to read some OTHER sequence — and it pins the track to that source even when the assembly changes. Delete it unless that is what you meant',
45
+ };
46
+ // The warnings a slot table carries: a legacy key or value a migration
47
+ // rewrites, and a declared key JBrowse fills in for itself.
48
+ function warnSlots(obj, entry, where, report) {
49
+ const declared = new Set(entry.slots.map(s => s.name));
50
+ for (const [key, value] of Object.entries(obj)) {
51
+ if (declared.has(key) && SELF_SUPPLIED[key]) {
52
+ report.warn(`${where}.${key}`, SELF_SUPPLIED[key]);
53
+ }
54
+ else if (entry.legacyValues?.[key]?.includes(value)) {
55
+ report.warn(`${where}.${key}`, `${JSON.stringify(value)} is a legacy value of "${key}" that a migration rewrites — the config loads, but the current spelling is one of the enum's members`);
56
+ }
57
+ else if (!declared.has(key) && entry.legacyKeys?.includes(key)) {
58
+ report.warn(`${where}.${key}`, `"${key}" is a legacy key that a migration rewrites into current slots — the config loads, but check the settings inside it landed`);
59
+ }
60
+ }
61
+ for (const slot of entry.slots) {
62
+ const value = obj[slot.name];
63
+ if (!slot.subSlots) {
64
+ continue;
65
+ }
66
+ for (const [item, suffix] of isRecord(value)
67
+ ? [[value, '']]
68
+ : Array.isArray(value)
69
+ ? value.map((v, i) => [v, `[${i}]`])
70
+ : []) {
71
+ if (isRecord(item)) {
72
+ warnSlots(item, { slots: slot.subSlots }, `${where}.${slot.name}${suffix}`, report);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ // Resolves a `type` against one of the manifest's groups. An unregistered type
78
+ // is a warning: MST is loud about it on load, and a plugin registers types the
79
+ // manifest never saw.
80
+ function resolveType(obj, group, groupLabel, where, report) {
81
+ const typeName = obj.type;
82
+ if (typeof typeName !== 'string') {
83
+ return undefined;
84
+ }
85
+ const canonical = typeName in group
86
+ ? typeName
87
+ : Object.keys(group).find(name => group[name].aliases?.includes(typeName));
88
+ const entry = canonical === undefined ? undefined : group[canonical];
89
+ if (entry && canonical !== undefined) {
90
+ return { ...entry, canonicalName: canonical };
91
+ }
92
+ report.warn(where, `${groupLabel} type "${typeName}" is not registered by the core plugins${didYouMean(typeName, Object.keys(group))} — it may come from a plugin`);
93
+ return undefined;
94
+ }
95
+ function checkPluggable(obj, group, groupLabel, where, report) {
96
+ if (!isRecord(obj)) {
97
+ return;
98
+ }
99
+ const entry = resolveType(obj, group, groupLabel, `${where}.type`, report);
100
+ if (entry) {
101
+ warnSlots(obj, entry, where, report);
102
+ }
103
+ }
104
+ function checkTrack(track, index, manifest, report, ctx) {
105
+ const where = `tracks[${index}]`;
106
+ const loose = typeof track.uri === 'string' && !('adapter' in track);
107
+ const trackId = track.trackId;
108
+ if (typeof trackId === 'string' && trackId) {
109
+ if (ctx.seenTrackIds.has(trackId)) {
110
+ report.error(`${where}.trackId`, `duplicate trackId "${trackId}" — a later track with the same id shadows the earlier one`);
111
+ }
112
+ else {
113
+ ctx.seenTrackIds.add(trackId);
114
+ }
115
+ }
116
+ const names = track.assemblyNames;
117
+ if (!Array.isArray(names) || names.length === 0) {
118
+ if (!loose || ctx.assemblyCount !== 1) {
119
+ report.error(`${where}.assemblyNames`, 'missing "assemblyNames"');
120
+ }
121
+ }
122
+ else {
123
+ for (const name of names) {
124
+ if (typeof name === 'string' && !ctx.assemblyNames.has(name)) {
125
+ // The connection caveat leads and the spelling guess trails: a
126
+ // connection added at runtime can supply the assembly, and a guess
127
+ // called a working config a typo.
128
+ report.error(`${where}.assemblyNames`, `assembly "${name}" is not defined in this config, and no connection here supplies one — though a connection added at runtime can${didYouMean(name, [...ctx.assemblyNames])}`);
129
+ }
130
+ }
131
+ }
132
+ checkPluggable(track, manifest.tracks, 'track', where, report);
133
+ if (!loose) {
134
+ checkPluggable(track.adapter, manifest.adapters, 'adapter', `${where}.adapter`, report);
135
+ }
136
+ const textSearching = track.textSearching;
137
+ if (isRecord(textSearching)) {
138
+ checkPluggable(textSearching.textSearchAdapter, manifest.textSearchAdapters, 'text search adapter', `${where}.textSearching.textSearchAdapter`, report);
139
+ }
140
+ if (Array.isArray(track.displays)) {
141
+ for (const [i, display] of track.displays.entries()) {
142
+ checkPluggable(display, manifest.displays, 'display', `${where}.displays[${i}]`, report);
143
+ }
144
+ }
145
+ }
146
+ function checkAssembly(assembly, index, manifest, report) {
147
+ const sequence = assembly.sequence;
148
+ if (isRecord(sequence) && isRecord(sequence.adapter)) {
149
+ checkPluggable(sequence.adapter, manifest.adapters, 'adapter', `assemblies[${index}].sequence.adapter`, report);
150
+ }
151
+ }
152
+ // A display node inside a session is instantiated by the display's state
153
+ // model; the schema reports a key it does not declare. What remains here is
154
+ // the legacy display-instance key a session migration still lifts.
155
+ function checkSessionDisplay(display, manifest, where, report) {
156
+ const entry = resolveType(display, manifest.displays, 'display', `${where}.type`, report);
157
+ if (!entry?.stateModelProps) {
158
+ return;
159
+ }
160
+ const migrated = new Set([
161
+ ...(manifest.migratedDisplayKeys['*'] ?? []),
162
+ ...(manifest.migratedDisplayKeys[entry.canonicalName] ?? []),
163
+ ]);
164
+ for (const key of Object.keys(display)) {
165
+ if (!entry.stateModelProps.includes(key) && migrated.has(key)) {
166
+ report.warn(`${where}.${key}`, `"${key}" is a legacy display-instance key that a session migration lifts onto the config slot replacing it — it still works, but writing the current slot is clearer`);
167
+ }
168
+ }
169
+ }
170
+ // Walks the tracks of a view and its sub-views (a synteny view holds a row of
171
+ // LGVs, each with tracks of its own).
172
+ function checkSessionViewTracks(view, manifest, where, report) {
173
+ if (!isRecord(view)) {
174
+ return;
175
+ }
176
+ if (Array.isArray(view.tracks)) {
177
+ for (const [i, track] of view.tracks.entries()) {
178
+ if (!isRecord(track) || !Array.isArray(track.displays)) {
179
+ continue;
180
+ }
181
+ for (const [j, display] of track.displays.entries()) {
182
+ if (isRecord(display)) {
183
+ checkSessionDisplay(display, manifest, `${where}.tracks[${i}].displays[${j}]`, report);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ if (Array.isArray(view.views)) {
189
+ for (const [i, sub] of view.views.entries()) {
190
+ checkSessionViewTracks(sub, manifest, `${where}.views[${i}]`, report);
191
+ }
192
+ }
193
+ }
194
+ // The ids a view names, which nothing validates at load: a trackId that does not
195
+ // exist simply fails to open, which reads as a rendering bug rather than a typo.
196
+ function checkViewReferences(view, where, report, ctx) {
197
+ const assembly = view.assembly;
198
+ if (typeof assembly === 'string' && !ctx.assemblyNames.has(assembly)) {
199
+ report.error(`${where}.assembly`, `assembly "${assembly}" is not defined in this config${didYouMean(assembly, [...ctx.assemblyNames])}`);
200
+ }
201
+ if (!Array.isArray(view.tracks)) {
202
+ return;
203
+ }
204
+ // A LinearSyntenyView's `tracks` is one array per level, so flatten one level
205
+ // before reading entries.
206
+ const entries = view.tracks.flatMap((t) => Array.isArray(t) ? t : [t]);
207
+ for (const [j, entry] of entries.entries()) {
208
+ // A built track snapshot names its config through `configuration` rather
209
+ // than `trackId`, and is state MST restores rather than a reference to open.
210
+ if (isRecord(entry) && !('trackId' in entry)) {
211
+ continue;
212
+ }
213
+ const trackId = isRecord(entry) ? entry.trackId : entry;
214
+ if (typeof trackId === 'string' &&
215
+ !ctx.seenTrackIds.has(trackId) &&
216
+ !ctx.sequenceTrackIds.has(trackId)) {
217
+ const message = `trackId "${trackId}" is not defined in this config${didYouMean(trackId, [...ctx.seenTrackIds, ...ctx.sequenceTrackIds])}`;
218
+ if (ctx.declaresPlugins) {
219
+ report.warn(`${where}.tracks[${j}]`, `${message} — a plugin it declares may create it`);
220
+ }
221
+ else {
222
+ report.error(`${where}.tracks[${j}]`, message);
223
+ }
224
+ }
225
+ }
226
+ }
227
+ function checkSessionView(view, manifest, where, report, ctx) {
228
+ const entry = resolveType(view, manifest.views, 'view', `${where}.type`, report);
229
+ if (!entry) {
230
+ return;
231
+ }
232
+ checkViewReferences(view, where, report, ctx);
233
+ // A warning, not an error: v5 unwraps the nesting on the way in, so the
234
+ // settings do arrive.
235
+ if (view.init !== undefined) {
236
+ report.warn(`${where}.init`, 'settings nested under "init", which is deprecated: write every setting directly on the view object');
237
+ if (isRecord(view.init)) {
238
+ checkViewReferences(view.init, `${where}.init`, report, ctx);
239
+ }
240
+ }
241
+ // A synteny or breakpoint row is a whole view snapshot of its own. A row
242
+ // written as a recipe names no view type, and which one it becomes is the
243
+ // parent launcher's business.
244
+ if (Array.isArray(view.views)) {
245
+ for (const [i, row] of view.views.entries()) {
246
+ if (isRecord(row) && typeof row.type === 'string') {
247
+ checkSessionView(row, manifest, `${where}.views[${i}]`, report, ctx);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ function checkSession(session, manifest, report, ctx) {
253
+ if (!isRecord(session) || !Array.isArray(session.views)) {
254
+ return;
255
+ }
256
+ for (const [i, view] of session.views.entries()) {
257
+ checkSessionViewTracks(view, manifest, `defaultSession.views[${i}]`, report);
258
+ }
259
+ for (const [i, view] of session.views.entries()) {
260
+ if (isRecord(view)) {
261
+ checkSessionView(view, manifest, `defaultSession.views[${i}]`, report, ctx);
262
+ }
263
+ }
264
+ }
265
+ const MARK_DISPLAY = 'LinearMarkDisplay';
266
+ const SLOTS_POINTER = `/$defs/${MARK_DISPLAY}Slots/properties`;
267
+ // The rule list reads a config snapshot, which is what a file is once the
268
+ // schema passes its keys and types and the manifest's lifts are applied. An
269
+ // entry the schema refuses is the schema's to report, and stands as a gap so
270
+ // its siblings keep their indices and their own reports. A list the file
271
+ // leaves unwritten is the slot's default entries, a display's default plot.
272
+ function declaredEntries(lifted, display, slot, slots) {
273
+ const list = lifted[slot];
274
+ const written = display[slot];
275
+ if (!Array.isArray(written)) {
276
+ return (slots.find(s => s.name === slot)?.defaultEntries ?? []);
277
+ }
278
+ return Array.isArray(list)
279
+ ? list.map((entry, i) => hasDeclaredShape(written[i], `${SLOTS_POINTER}/${slot}/items`)
280
+ ? entry
281
+ : undefined)
282
+ : [];
283
+ }
284
+ function slotPath(slot) {
285
+ return slot.replaceAll(/\.(\d+)(?=\.|$)/g, '[$1]');
286
+ }
287
+ // The display types that read a marks list: the mark display and every one
288
+ // built on it, which the manifest tells apart by the `marks` slot each lists.
289
+ function isMarkDisplayType(type, manifest) {
290
+ return (typeof type === 'string' &&
291
+ !!manifest.displays[type]?.slots.some(slot => slot.name === 'marks'));
292
+ }
293
+ // A mark display's own entry, whose marks may be its type's default plot, or
294
+ // a `displayDefaults` holding marks. A mark is a record with a `transform` of
295
+ // its own, so an untyped node counts only by its `marks`.
296
+ function holdsMarkRules(node, manifest) {
297
+ return (isMarkDisplayType(node.type, manifest) ||
298
+ (node.type === undefined && Array.isArray(node.marks)));
299
+ }
300
+ // What the marks of one display say together that it cannot draw as written:
301
+ // the rule list the display shows in its corner notice, copied from the plugin
302
+ // (`markRules/`). A single mark's `requires` is the schema's to report.
303
+ function checkMarkDisplay(display, manifest, where, report) {
304
+ const type = typeof display.type === 'string' ? display.type : MARK_DISPLAY;
305
+ const slots = manifest.displays[type]?.slots;
306
+ const lifted = liftToSnapshot(display, slots ?? []);
307
+ if (!slots || !isRecord(lifted)) {
308
+ return;
309
+ }
310
+ const { facet, rows, scales } = lifted;
311
+ for (const { level, rule, mark, slot, message } of markProblems({
312
+ marks: declaredEntries(lifted, display, 'marks', slots),
313
+ facet: isRecord(facet) ? facet : undefined,
314
+ transform: declaredEntries(lifted, display, 'transform', slots),
315
+ rows: isRecord(rows) ? rows : undefined,
316
+ scales: isRecord(scales) ? scalesOf(scales) : undefined,
317
+ })) {
318
+ pushOnce(report, {
319
+ level,
320
+ where: `${where}${mark === undefined ? '' : `.marks[${mark}]`}.${slotPath(slot)}`,
321
+ message,
322
+ rule,
323
+ });
324
+ }
325
+ }
326
+ // A problem the colour and scale rules and the mark rule list both reach, such
327
+ // as scales.y's ends on a mark display, is reported once.
328
+ function pushOnce(report, problem) {
329
+ if (!report.problems.some(p => p.where === problem.where && p.rule === problem.rule)) {
330
+ report.problems.push(problem);
331
+ }
332
+ }
333
+ function numberOf(value) {
334
+ return typeof value === 'number' ? value : undefined;
335
+ }
336
+ function scaleEndsOf(scale) {
337
+ return {
338
+ domainMin: numberOf(scale.domainMin),
339
+ domainMax: numberOf(scale.domainMax),
340
+ domainQuantile: numberOf(scale.domainQuantile),
341
+ };
342
+ }
343
+ function scalesOf(scales) {
344
+ return isRecord(scales.y) ? { y: scaleEndsOf(scales.y) } : {};
345
+ }
346
+ function declaredMembers(written, slot) {
347
+ const declared = new Set((slot.subSlots ?? []).map(s => s.name));
348
+ return Object.fromEntries(Object.entries(written).filter(([key]) => declared.has(key)));
349
+ }
350
+ // What the display's colour objects and its value scale say together that it
351
+ // cannot draw as written: its schema's fieldPresets stand in for the plugin
352
+ // code the CLI does not run, and every quantitative scale's ends answer to
353
+ // one rule.
354
+ function checkScaleSlots(display, slots, where, report) {
355
+ const lifted = liftToSnapshot(display, slots);
356
+ if (!isRecord(lifted)) {
357
+ return;
358
+ }
359
+ const warn = (at, { rule, slot, message }) => {
360
+ pushOnce(report, {
361
+ level: 'warning',
362
+ where: `${at}.${slot}`,
363
+ message,
364
+ rule,
365
+ });
366
+ };
367
+ for (const slot of slots) {
368
+ const written = lifted[slot.name];
369
+ if (!isRecord(written)) {
370
+ continue;
371
+ }
372
+ const members = declaredMembers(written, slot);
373
+ if (slot.fieldPresets) {
374
+ for (const problem of colorProblems({
375
+ ...scaleEndsOf(members),
376
+ field: typeof members.field === 'string' ? members.field : '',
377
+ scale: typeof members.scale === 'string' ? members.scale : undefined,
378
+ domain: Array.isArray(members.domain) ? members.domain : undefined,
379
+ range: Array.isArray(members.range) ? members.range : undefined,
380
+ labels: Array.isArray(members.labels) ? members.labels : undefined,
381
+ }, slot.fieldPresets)) {
382
+ warn(`${where}.${slot.name}`, problem);
383
+ }
384
+ }
385
+ if (slot.name === 'scales' && isRecord(members.y)) {
386
+ for (const problem of scaleEndProblems(scaleEndsOf(members.y))) {
387
+ warn(`${where}.scales.y`, problem);
388
+ }
389
+ }
390
+ }
391
+ }
392
+ // A marks list sits wherever a config takes a mark display's slots: a
393
+ // track's `displays` entry, its `displayDefaults`, a session's inline track
394
+ // entry. Only a mark display declares `marks`, so the walk is over the whole
395
+ // file, past a place where the schema refused the key: a session display node
396
+ // drops it.
397
+ function checkMarkDisplays(node, manifest, where, report) {
398
+ if (Array.isArray(node)) {
399
+ for (const [i, item] of node.entries()) {
400
+ checkMarkDisplays(item, manifest, `${where}[${i}]`, report);
401
+ }
402
+ }
403
+ else if (isRecord(node)) {
404
+ const slots = typeof node.type === 'string'
405
+ ? manifest.displays[node.type]?.slots
406
+ : undefined;
407
+ if (slots) {
408
+ checkScaleSlots(node, slots, where, report);
409
+ }
410
+ if (typeof node.type === 'string' && isRecord(node.displayDefaults)) {
411
+ for (const display of displayDefaultsForTrackType(node.type, manifest)
412
+ .displayTypes) {
413
+ checkScaleSlots(node.displayDefaults, manifest.displays[display]?.slots ?? [], `${where}.displayDefaults`, report);
414
+ }
415
+ }
416
+ if (holdsMarkRules(node, manifest) &&
417
+ !report.problems.some(p => p.where === `${where}.marks`)) {
418
+ checkMarkDisplay(node, manifest, where, report);
419
+ }
420
+ for (const [key, member] of Object.entries(node)) {
421
+ checkMarkDisplays(member, manifest, where ? `${where}.${key}` : key, report);
422
+ }
423
+ }
424
+ }
425
+ export function validateConfig(config, manifest = configManifest) {
426
+ const report = new Report();
427
+ if (!isRecord(config)) {
428
+ report.error('', 'config is not a JSON object');
429
+ return report.result();
430
+ }
431
+ const declaresPlugins = Array.isArray(config.plugins) && config.plugins.length > 0;
432
+ if (declaresPlugins) {
433
+ report.note(`config declares ${config.plugins.length} plugin(s); the types and slots they register are not in the schema and cannot be checked`);
434
+ }
435
+ const { configuration } = config;
436
+ const pluginSlots = declaresPlugins && isRecord(configuration)
437
+ ? Object.keys(configuration).filter(k => !rootConfigurationSlots.has(k))
438
+ : [];
439
+ for (const key of pluginSlots) {
440
+ report.warn(`configuration.${key}`, `core declares no "${key}" slot${didYouMean(key, [...rootConfigurationSlots])} — a plugin this config declares may`);
441
+ }
442
+ report.problems.push(...schemaProblems(pluginSlots.length && isRecord(configuration)
443
+ ? {
444
+ ...config,
445
+ configuration: Object.fromEntries(Object.entries(configuration).filter(([k]) => !pluginSlots.includes(k))),
446
+ }
447
+ : config));
448
+ const assemblies = (Array.isArray(config.assemblies)
449
+ ? config.assemblies
450
+ : isRecord(config.assembly)
451
+ ? [config.assembly]
452
+ : []).filter(isRecord);
453
+ if (assemblies.length === 0) {
454
+ report.error('assemblies', 'no assemblies — a config needs at least one');
455
+ }
456
+ const ctx = {
457
+ // An assembly's `aliases` are usable wherever its name is.
458
+ assemblyNames: new Set(assemblies
459
+ .flatMap(a => [a.name, ...(Array.isArray(a.aliases) ? a.aliases : [])])
460
+ .filter((n) => typeof n === 'string')),
461
+ assemblyCount: assemblies.length,
462
+ seenTrackIds: new Set(),
463
+ declaresPlugins,
464
+ // Both the written trackId and the one the `{name, uri}` shorthand derives,
465
+ // since a config using the shorthand still gets the derived id at runtime.
466
+ sequenceTrackIds: new Set(assemblies.flatMap(a => {
467
+ const declared = isRecord(a.sequence) ? a.sequence.trackId : undefined;
468
+ return [
469
+ ...(typeof declared === 'string' ? [declared] : []),
470
+ ...(typeof a.name === 'string'
471
+ ? [`${a.name}-ReferenceSequenceTrack`]
472
+ : []),
473
+ ];
474
+ })),
475
+ };
476
+ for (const [i, assembly] of assemblies.entries()) {
477
+ checkAssembly(assembly, i, manifest, report);
478
+ }
479
+ const tracks = Array.isArray(config.tracks)
480
+ ? config.tracks.filter(isRecord)
481
+ : [];
482
+ for (const [i, track] of tracks.entries()) {
483
+ checkTrack(track, i, manifest, report, ctx);
484
+ }
485
+ const connections = Array.isArray(config.connections)
486
+ ? config.connections
487
+ : [];
488
+ for (const [i, connection] of connections.entries()) {
489
+ checkPluggable(connection, manifest.connections, 'connection', `connections[${i}]`, report);
490
+ }
491
+ const aggregate = Array.isArray(config.aggregateTextSearchAdapters)
492
+ ? config.aggregateTextSearchAdapters
493
+ : [];
494
+ for (const [i, adapter] of aggregate.entries()) {
495
+ checkPluggable(adapter, manifest.textSearchAdapters, 'text search adapter', `aggregateTextSearchAdapters[${i}]`, report);
496
+ }
497
+ // Tracks are registered before the session is checked, so a session may
498
+ // reference any track in the file regardless of declaration order.
499
+ checkSession(config.defaultSession, manifest, report, ctx);
500
+ checkMarkDisplays(config, manifest, '', report);
501
+ return report.result();
502
+ }