@jbrowse/cli 5.0.0-beta.8 → 5.0.0-beta.9
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/README.md +31 -23
- package/dist/commands/add-track-utils/track-config.js +2 -4
- package/dist/commands/add-track.js +1 -2
- package/dist/commands/make-pif/index.js +2 -2
- package/dist/commands/validate/configManifest.generated.js +1974 -1370
- package/dist/commands/validate/configSchema.generated.js +5747 -3290
- package/dist/commands/validate/index.js +13 -4
- package/dist/commands/validate/liftConfig.js +29 -0
- package/dist/commands/validate/markRules/autoBin.js +27 -0
- package/dist/commands/validate/markRules/colorScale.js +81 -0
- package/dist/commands/validate/markRules/jexlStrings.js +23 -0
- package/dist/commands/validate/markRules/markProblems.js +253 -0
- package/dist/commands/validate/markRules/markRuleFacts.js +3 -0
- package/dist/commands/validate/markRules/markVocabulary.js +20 -0
- package/dist/commands/validate/markRules/shapeSpecs.js +41 -0
- package/dist/commands/validate/schemaValidate.js +145 -11
- package/dist/commands/validate/validateConfig.js +142 -7
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -34,8 +34,9 @@ function validatorFor(pointer) {
|
|
|
34
34
|
validateSchema: false,
|
|
35
35
|
code: { optimize: 0 },
|
|
36
36
|
});
|
|
37
|
+
const whole = ajv.getSchema(schemaId) ?? ajv.compile(root);
|
|
37
38
|
if (pointer === '') {
|
|
38
|
-
return
|
|
39
|
+
return whole;
|
|
39
40
|
}
|
|
40
41
|
const fn = ajv.getSchema(`${schemaId}#${pointer}`);
|
|
41
42
|
if (!fn) {
|
|
@@ -56,7 +57,7 @@ function resolve(schema) {
|
|
|
56
57
|
// slot table it intersects through allOf.
|
|
57
58
|
function acceptedKeys(schema) {
|
|
58
59
|
const own = Object.keys(schema.properties ?? {});
|
|
59
|
-
const composed = (schema.allOf ?? []).flatMap(branch => Object.keys(resolve(branch).properties));
|
|
60
|
+
const composed = (schema.allOf ?? []).flatMap(branch => Object.keys(resolve(branch).properties ?? {}));
|
|
60
61
|
return [...new Set([...own, ...composed])];
|
|
61
62
|
}
|
|
62
63
|
function describe(schema) {
|
|
@@ -67,6 +68,9 @@ function describe(schema) {
|
|
|
67
68
|
if (name === 'JexlString') {
|
|
68
69
|
return 'a "jexl:" expression';
|
|
69
70
|
}
|
|
71
|
+
if (name === 'CssColor') {
|
|
72
|
+
return 'a CSS color (a name like "red", "#rrggbb", "rgb()" or "hsl()")';
|
|
73
|
+
}
|
|
70
74
|
if (name) {
|
|
71
75
|
return `a ${name}`;
|
|
72
76
|
}
|
|
@@ -112,6 +116,15 @@ function join(where, key) {
|
|
|
112
116
|
function isRecord(value) {
|
|
113
117
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
114
118
|
}
|
|
119
|
+
function jsonTypeOf(data) {
|
|
120
|
+
return Array.isArray(data)
|
|
121
|
+
? 'array'
|
|
122
|
+
: data === null
|
|
123
|
+
? 'null'
|
|
124
|
+
: typeof data === 'object'
|
|
125
|
+
? 'object'
|
|
126
|
+
: typeof data;
|
|
127
|
+
}
|
|
115
128
|
function typeOf(data) {
|
|
116
129
|
return isRecord(data) && typeof data.type === 'string' ? data.type : undefined;
|
|
117
130
|
}
|
|
@@ -130,9 +143,23 @@ const viewNames = new Set(defs.View.properties.type.anyOf[0]
|
|
|
130
143
|
// so a slot name written there is dropped just like a misspelling — and the
|
|
131
144
|
// advice differs, because the key is real and there are two right places for
|
|
132
145
|
// it.
|
|
146
|
+
function listed(keys) {
|
|
147
|
+
return keys.length > 1
|
|
148
|
+
? `${keys.slice(0, -1).join(', ')} and ${keys.at(-1)}`
|
|
149
|
+
: (keys[0] ?? 'nothing');
|
|
150
|
+
}
|
|
151
|
+
// The keys Vega-Lite and GenomeSpy spell otherwise, by the schema that
|
|
152
|
+
// declares this spelling
|
|
153
|
+
const SPELLED_HERE = {
|
|
154
|
+
Mark: { mark: 'shape' },
|
|
155
|
+
MarkEncoding: { shape: 'glyph' },
|
|
156
|
+
};
|
|
133
157
|
function unknownKeyMessage(key, schema, accepted) {
|
|
134
158
|
const title = typeof schema.title === 'string' ? schema.title : '';
|
|
135
|
-
const
|
|
159
|
+
const here = SPELLED_HERE[title]?.[key];
|
|
160
|
+
const guess = here
|
|
161
|
+
? ` — Vega-Lite's "${key}" is spelled "${here}" here`
|
|
162
|
+
: didYouMean(key, accepted);
|
|
136
163
|
if (key === 'sequenceAdapter' && !title.endsWith('Snapshot')) {
|
|
137
164
|
return 'JBrowse takes the sequence from the assembly the track is displayed against, and this adapter declares no `sequenceAdapter` slot, so the one written here is never read — delete it';
|
|
138
165
|
}
|
|
@@ -157,11 +184,39 @@ function unknownKeyMessage(key, schema, accepted) {
|
|
|
157
184
|
if (title.endsWith('TrackEntry')) {
|
|
158
185
|
return `"${key}" is neither a config slot nor a property of ${title.replace(/TrackEntry$/, '')}${guess} — the entry's keys are folded onto that display, and one it does not declare silently does nothing`;
|
|
159
186
|
}
|
|
187
|
+
if (schema['x-closed'] === true) {
|
|
188
|
+
return `unknown slot "${key}"${guess} — ${title} takes ${listed(accepted)}, and JBrowse refuses to load it rather than drop a key it does not declare`;
|
|
189
|
+
}
|
|
160
190
|
return `unknown slot "${key}"${guess} — JBrowse ignores keys it does not declare, so this setting silently does nothing`;
|
|
161
191
|
}
|
|
162
192
|
function isJexl(value) {
|
|
163
193
|
return typeof value === 'string' && value.startsWith('jexl:');
|
|
164
194
|
}
|
|
195
|
+
// The keywords a slot declaring no contextVariable fails a `jexl:` string on:
|
|
196
|
+
// a plain string's `not`, a number's `type`, a color's `pattern`, an enum's
|
|
197
|
+
// own, and a union of those
|
|
198
|
+
const REFUSES_CALLBACK = new Set([
|
|
199
|
+
'not',
|
|
200
|
+
'type',
|
|
201
|
+
'pattern',
|
|
202
|
+
'enum',
|
|
203
|
+
'const',
|
|
204
|
+
'anyOf',
|
|
205
|
+
]);
|
|
206
|
+
function refusedCallback(value) {
|
|
207
|
+
return `takes no "jexl:" callback, and ${JSON.stringify(value)} is one: a slot that takes a callback lists its callback args in the config docs`;
|
|
208
|
+
}
|
|
209
|
+
function isScalar(schema) {
|
|
210
|
+
const resolved = resolve(schema);
|
|
211
|
+
return (resolved.properties === undefined &&
|
|
212
|
+
resolved.items === undefined &&
|
|
213
|
+
resolved.allOf === undefined &&
|
|
214
|
+
resolved.type !== 'object' &&
|
|
215
|
+
resolved.type !== 'array');
|
|
216
|
+
}
|
|
217
|
+
function isJexlBranch(schema) {
|
|
218
|
+
return refName(schema) === 'JexlString';
|
|
219
|
+
}
|
|
165
220
|
function branchesOf(error) {
|
|
166
221
|
return error.parentSchema.anyOf;
|
|
167
222
|
}
|
|
@@ -176,6 +231,11 @@ function pickBranch(error, data, errors) {
|
|
|
176
231
|
return byType;
|
|
177
232
|
}
|
|
178
233
|
}
|
|
234
|
+
const jsonType = jsonTypeOf(data);
|
|
235
|
+
const byJsonType = branches.flatMap((b, i) => (b.type ?? resolve(b).type) === jsonType ? [i] : []);
|
|
236
|
+
if (byJsonType.length === 1) {
|
|
237
|
+
return byJsonType[0];
|
|
238
|
+
}
|
|
179
239
|
if (isRecord(data)) {
|
|
180
240
|
const tagged = branches.findIndex(b => 'locationType' in data &&
|
|
181
241
|
admitsLocation(resolve(b), data.locationType));
|
|
@@ -199,6 +259,31 @@ function admitsLocation(schema, locationType) {
|
|
|
199
259
|
const prop = schema.properties?.locationType;
|
|
200
260
|
return prop?.const === locationType;
|
|
201
261
|
}
|
|
262
|
+
// A schema's `requires` entry, which the generator writes as an `if`/`then`
|
|
263
|
+
// annotated with the entry's id, the slot path it requires and its message.
|
|
264
|
+
function requirementOf(error) {
|
|
265
|
+
const parent = error.parentSchema ?? {};
|
|
266
|
+
const stated = parent['x-requirement'];
|
|
267
|
+
return error.keyword === 'if' && isRecord(stated)
|
|
268
|
+
? {
|
|
269
|
+
id: String(stated.id),
|
|
270
|
+
slot: String(stated.slot),
|
|
271
|
+
message: String(parent.errorMessage),
|
|
272
|
+
branch: `${error.schemaPath.replace(/\/if$/, '')}/then/`,
|
|
273
|
+
}
|
|
274
|
+
: undefined;
|
|
275
|
+
}
|
|
276
|
+
// Whether a failure is one requirement's: the `if` itself, or an error under
|
|
277
|
+
// its `then` at or beneath the object the `if` judged.
|
|
278
|
+
function statesRequirement(error, errors) {
|
|
279
|
+
return errors.some(other => {
|
|
280
|
+
const requirement = requirementOf(other);
|
|
281
|
+
return (requirement !== undefined &&
|
|
282
|
+
(other === error ||
|
|
283
|
+
(error.schemaPath.startsWith(requirement.branch) &&
|
|
284
|
+
error.instancePath.startsWith(other.instancePath))));
|
|
285
|
+
});
|
|
286
|
+
}
|
|
202
287
|
function requiredOnly(branches) {
|
|
203
288
|
return branches.every(b => Object.keys(b).length === 1 && Array.isArray(b.required));
|
|
204
289
|
}
|
|
@@ -207,25 +292,48 @@ function requiredOnly(branches) {
|
|
|
207
292
|
// than one per alternative.
|
|
208
293
|
function explain(data, errors, problems, depth) {
|
|
209
294
|
const covered = new Set();
|
|
210
|
-
const emit = (where, message) => {
|
|
211
|
-
const key = `${where}
|
|
295
|
+
const emit = (where, message, rule) => {
|
|
296
|
+
const key = `${where}\0${message}`;
|
|
212
297
|
if (!covered.has(key)) {
|
|
213
298
|
covered.add(key);
|
|
214
|
-
problems.push({
|
|
299
|
+
problems.push({
|
|
300
|
+
level: 'error',
|
|
301
|
+
where,
|
|
302
|
+
message,
|
|
303
|
+
...(rule ? { rule } : {}),
|
|
304
|
+
});
|
|
215
305
|
}
|
|
216
306
|
};
|
|
217
307
|
// A union failure explains everything beneath it; the branch is re-validated
|
|
218
308
|
// on its own so the sub-errors reported for the other branches drop out.
|
|
309
|
+
// A union nested as another's branch at the same path (`number | "auto"`
|
|
310
|
+
// inside a slot's `value | jexl:`) is the outer one's to explain, and never
|
|
311
|
+
// the other way round, or neither is reported.
|
|
219
312
|
const unions = errors.filter(e => e.keyword === 'anyOf');
|
|
313
|
+
const isBranchOf = (u, e) => branchesOf(u).some(b => resolve(b) === e.parentSchema);
|
|
220
314
|
const shadowed = (e) => unions.some(u => u !== e &&
|
|
221
|
-
(e.instancePath
|
|
222
|
-
e.instancePath
|
|
315
|
+
(e.instancePath.startsWith(`${u.instancePath}/`) ||
|
|
316
|
+
(e.instancePath === u.instancePath &&
|
|
317
|
+
!(e.keyword === 'anyOf' && isBranchOf(e, u)))));
|
|
223
318
|
for (const error of errors) {
|
|
224
|
-
if (shadowed(error)
|
|
319
|
+
if (shadowed(error)) {
|
|
225
320
|
continue;
|
|
226
321
|
}
|
|
227
322
|
const where = pathOf(error.instancePath);
|
|
323
|
+
const requirement = requirementOf(error);
|
|
324
|
+
if (requirement) {
|
|
325
|
+
emit(join(where, requirement.slot), requirement.message, requirement.id);
|
|
326
|
+
}
|
|
327
|
+
if (error.keyword === 'if' || statesRequirement(error, errors)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
228
330
|
const parent = error.parentSchema;
|
|
331
|
+
if (isJexl(error.data) &&
|
|
332
|
+
REFUSES_CALLBACK.has(error.keyword) &&
|
|
333
|
+
!(error.keyword === 'anyOf' && branchesOf(error).some(isJexlBranch))) {
|
|
334
|
+
emit(where, refusedCallback(error.data));
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
229
337
|
switch (error.keyword) {
|
|
230
338
|
case 'additionalProperties':
|
|
231
339
|
case 'unevaluatedProperties': {
|
|
@@ -243,12 +351,18 @@ function explain(data, errors, problems, depth) {
|
|
|
243
351
|
emit(where, `needs one of ${branches.map(b => `"${b.required[0]}"`).join(', ')}`);
|
|
244
352
|
break;
|
|
245
353
|
}
|
|
246
|
-
const jexlBranch = branches.findIndex(
|
|
354
|
+
const jexlBranch = branches.findIndex(isJexlBranch);
|
|
247
355
|
if (jexlBranch >= 0 && !isJexl(value)) {
|
|
248
356
|
const others = branches.filter((_, i) => i !== jexlBranch);
|
|
249
357
|
emit(where, `expected ${others.map(describe).join(' or ')} or a "jexl:" expression, got ${JSON.stringify(value)}`);
|
|
250
358
|
break;
|
|
251
359
|
}
|
|
360
|
+
if (!isRecord(value) &&
|
|
361
|
+
!Array.isArray(value) &&
|
|
362
|
+
branches.every(isScalar)) {
|
|
363
|
+
emit(where, `expected ${branches.map(describe).join(' or ')}, got ${JSON.stringify(value)}`);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
252
366
|
if (depth > 8) {
|
|
253
367
|
emit(where, `does not match any accepted form`);
|
|
254
368
|
break;
|
|
@@ -272,10 +386,14 @@ function explain(data, errors, problems, depth) {
|
|
|
272
386
|
}
|
|
273
387
|
break;
|
|
274
388
|
}
|
|
389
|
+
case 'pattern':
|
|
390
|
+
emit(where, error.schemaPath.endsWith('/CssColor/pattern')
|
|
391
|
+
? `expected ${describe({ $ref: '#/$defs/CssColor' })}, got ${JSON.stringify(error.data)}`
|
|
392
|
+
: `expected a string matching /${String(parent.pattern)}/, got ${JSON.stringify(error.data)}`);
|
|
393
|
+
break;
|
|
275
394
|
case 'type':
|
|
276
395
|
case 'enum':
|
|
277
396
|
case 'const':
|
|
278
|
-
case 'pattern':
|
|
279
397
|
emit(where, `expected ${describe(parent)}, got ${JSON.stringify(error.data)}`);
|
|
280
398
|
break;
|
|
281
399
|
case 'not':
|
|
@@ -297,4 +415,20 @@ export function schemaProblems(data, pointer = '') {
|
|
|
297
415
|
explain(data, validate.errors ?? [], problems, 0);
|
|
298
416
|
return problems;
|
|
299
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Whether `data` has the keys and value types the definition at `pointer`
|
|
420
|
+
* declares, whatever its `requires` entries say of it: the contract a rule
|
|
421
|
+
* list reads a config under, since a load refuses a key or a type and no
|
|
422
|
+
* combination of slots.
|
|
423
|
+
*/
|
|
424
|
+
export function hasDeclaredShape(data, pointer) {
|
|
425
|
+
const validate = validatorFor(pointer);
|
|
426
|
+
const errors = validate(data) ? [] : (validate.errors ?? []);
|
|
427
|
+
return errors.every(error => statesRequirement(error, errors));
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* The keys core declares directly under `configuration`. A plugin adds its own
|
|
431
|
+
* beside them, the way `LinearGenomeViewPlugin` is core's.
|
|
432
|
+
*/
|
|
433
|
+
export const rootConfigurationSlots = new Set(Object.keys(defs.RootConfiguration.properties));
|
|
300
434
|
export { schemaId as configSchemaUrl };
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Pure: no filesystem, no process exit. The command wrapper owns both.
|
|
11
11
|
import { configManifest } from './configManifest.generated.js';
|
|
12
|
-
import {
|
|
12
|
+
import { displayDefaultsForTrackType } from './displayDefaultKeys.js';
|
|
13
|
+
import { isRecord, liftToSnapshot } from './liftConfig.js';
|
|
14
|
+
import { colorProblems, fieldScaleOf } from './markRules/colorScale.js';
|
|
15
|
+
import { markProblems } from './markRules/markProblems.js';
|
|
16
|
+
import { hasDeclaredShape, rootConfigurationSlots, schemaProblems, } from './schemaValidate.js';
|
|
13
17
|
import { didYouMean } from './suggest.js';
|
|
14
18
|
class Report {
|
|
15
19
|
problems = [];
|
|
@@ -32,9 +36,6 @@ class Report {
|
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
|
-
function isRecord(value) {
|
|
36
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
39
|
// A declared `sequenceAdapter` works and is simply the wrong way round: every
|
|
39
40
|
// feature adapter takes its sequence from the assembly the track is displayed
|
|
40
41
|
// against, so a hand-written one pins the track to that source. An undeclared
|
|
@@ -213,7 +214,13 @@ function checkViewReferences(view, where, report, ctx) {
|
|
|
213
214
|
if (typeof trackId === 'string' &&
|
|
214
215
|
!ctx.seenTrackIds.has(trackId) &&
|
|
215
216
|
!ctx.sequenceTrackIds.has(trackId)) {
|
|
216
|
-
|
|
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
|
+
}
|
|
217
224
|
}
|
|
218
225
|
}
|
|
219
226
|
}
|
|
@@ -255,16 +262,142 @@ function checkSession(session, manifest, report, ctx) {
|
|
|
255
262
|
}
|
|
256
263
|
}
|
|
257
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.
|
|
271
|
+
function declaredEntries(lifted, display, slot) {
|
|
272
|
+
const list = lifted[slot];
|
|
273
|
+
const written = display[slot];
|
|
274
|
+
return Array.isArray(list) && Array.isArray(written)
|
|
275
|
+
? list.map((entry, i) => hasDeclaredShape(written[i], `${SLOTS_POINTER}/${slot}/items`)
|
|
276
|
+
? entry
|
|
277
|
+
: undefined)
|
|
278
|
+
: [];
|
|
279
|
+
}
|
|
280
|
+
function slotPath(slot) {
|
|
281
|
+
return slot.replaceAll(/\.(\d+)(?=\.|$)/g, '[$1]');
|
|
282
|
+
}
|
|
283
|
+
// A mark display's own entry, whose marks may come from `displayDefaults`, or
|
|
284
|
+
// a `displayDefaults` holding marks. A mark is a record with a `transform` of
|
|
285
|
+
// its own, so an untyped node counts only by its `marks`.
|
|
286
|
+
function holdsMarkRules(node) {
|
|
287
|
+
return node.type === MARK_DISPLAY
|
|
288
|
+
? Array.isArray(node.marks) || Array.isArray(node.transform)
|
|
289
|
+
: node.type === undefined && Array.isArray(node.marks);
|
|
290
|
+
}
|
|
291
|
+
// What the marks of one display say together that it cannot draw as written:
|
|
292
|
+
// the rule list the display shows in its corner notice, copied from the plugin
|
|
293
|
+
// (`markRules/`). A single mark's `requires` is the schema's to report.
|
|
294
|
+
function checkMarkDisplay(display, manifest, where, report) {
|
|
295
|
+
const slots = manifest.displays[MARK_DISPLAY]?.slots;
|
|
296
|
+
const lifted = liftToSnapshot(display, slots ?? []);
|
|
297
|
+
if (!slots || !isRecord(lifted)) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const { facet } = lifted;
|
|
301
|
+
for (const { level, rule, mark, slot, message } of markProblems(declaredEntries(lifted, display, 'marks'), isRecord(facet) ? facet : undefined, declaredEntries(lifted, display, 'transform'))) {
|
|
302
|
+
report.problems.push({
|
|
303
|
+
level,
|
|
304
|
+
where: `${where}${mark === undefined ? '' : `.marks[${mark}]`}.${slotPath(slot)}`,
|
|
305
|
+
message,
|
|
306
|
+
rule,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// The display's own colour rules, with its schema's fieldScale standing in
|
|
311
|
+
// for the plugin code the CLI does not run
|
|
312
|
+
function checkColorSlots(display, slots, where, report) {
|
|
313
|
+
const lifted = liftToSnapshot(display, slots);
|
|
314
|
+
if (!isRecord(lifted)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
for (const { name, fieldScale, subSlots = [] } of slots) {
|
|
318
|
+
const written = lifted[name];
|
|
319
|
+
if (!fieldScale || !isRecord(written)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const declared = new Set(subSlots.map(slot => slot.name));
|
|
323
|
+
const color = Object.fromEntries(Object.entries(written).filter(([key]) => declared.has(key)));
|
|
324
|
+
const field = typeof color.field === 'string' ? color.field : '';
|
|
325
|
+
for (const problem of colorProblems({
|
|
326
|
+
field,
|
|
327
|
+
scale: typeof color.scale === 'string' ? color.scale : undefined,
|
|
328
|
+
domain: Array.isArray(color.domain) ? color.domain : undefined,
|
|
329
|
+
range: Array.isArray(color.range) ? color.range : undefined,
|
|
330
|
+
domainMin: typeof color.domainMin === 'number' ? color.domainMin : undefined,
|
|
331
|
+
domainMax: typeof color.domainMax === 'number' ? color.domainMax : undefined,
|
|
332
|
+
}, fieldScaleOf(fieldScale, field))) {
|
|
333
|
+
const at = `${where}.${name}.${problem.slot}`;
|
|
334
|
+
if (!report.problems.some(p => p.where === at && p.rule === problem.rule)) {
|
|
335
|
+
report.problems.push({
|
|
336
|
+
level: 'warning',
|
|
337
|
+
where: at,
|
|
338
|
+
message: problem.message,
|
|
339
|
+
rule: problem.rule,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// A marks list sits wherever a config takes the mark display's slots: a
|
|
346
|
+
// track's `displays` entry, its `displayDefaults`, a session's inline track
|
|
347
|
+
// entry. No other type declares `marks`, so the walk is over the whole file,
|
|
348
|
+
// past a place where the schema refused the key: a session display node
|
|
349
|
+
// drops it.
|
|
350
|
+
function checkMarkDisplays(node, manifest, where, report) {
|
|
351
|
+
if (Array.isArray(node)) {
|
|
352
|
+
for (const [i, item] of node.entries()) {
|
|
353
|
+
checkMarkDisplays(item, manifest, `${where}[${i}]`, report);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else if (isRecord(node)) {
|
|
357
|
+
const colorSlots = typeof node.type === 'string'
|
|
358
|
+
? manifest.displays[node.type]?.slots
|
|
359
|
+
: undefined;
|
|
360
|
+
if (colorSlots) {
|
|
361
|
+
checkColorSlots(node, colorSlots, where, report);
|
|
362
|
+
}
|
|
363
|
+
if (typeof node.type === 'string' && isRecord(node.displayDefaults)) {
|
|
364
|
+
for (const display of displayDefaultsForTrackType(node.type, manifest)
|
|
365
|
+
.displayTypes) {
|
|
366
|
+
checkColorSlots(node.displayDefaults, manifest.displays[display]?.slots ?? [], `${where}.displayDefaults`, report);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (holdsMarkRules(node) &&
|
|
370
|
+
!report.problems.some(p => p.where === `${where}.marks`)) {
|
|
371
|
+
checkMarkDisplay(node, manifest, where, report);
|
|
372
|
+
}
|
|
373
|
+
for (const [key, member] of Object.entries(node)) {
|
|
374
|
+
checkMarkDisplays(member, manifest, where ? `${where}.${key}` : key, report);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
258
378
|
export function validateConfig(config, manifest = configManifest) {
|
|
259
379
|
const report = new Report();
|
|
260
380
|
if (!isRecord(config)) {
|
|
261
381
|
report.error('', 'config is not a JSON object');
|
|
262
382
|
return report.result();
|
|
263
383
|
}
|
|
264
|
-
|
|
384
|
+
const declaresPlugins = Array.isArray(config.plugins) && config.plugins.length > 0;
|
|
385
|
+
if (declaresPlugins) {
|
|
265
386
|
report.note(`config declares ${config.plugins.length} plugin(s); the types and slots they register are not in the schema and cannot be checked`);
|
|
266
387
|
}
|
|
267
|
-
|
|
388
|
+
const { configuration } = config;
|
|
389
|
+
const pluginSlots = declaresPlugins && isRecord(configuration)
|
|
390
|
+
? Object.keys(configuration).filter(k => !rootConfigurationSlots.has(k))
|
|
391
|
+
: [];
|
|
392
|
+
for (const key of pluginSlots) {
|
|
393
|
+
report.warn(`configuration.${key}`, `core declares no "${key}" slot${didYouMean(key, [...rootConfigurationSlots])} — a plugin this config declares may`);
|
|
394
|
+
}
|
|
395
|
+
report.problems.push(...schemaProblems(pluginSlots.length && isRecord(configuration)
|
|
396
|
+
? {
|
|
397
|
+
...config,
|
|
398
|
+
configuration: Object.fromEntries(Object.entries(configuration).filter(([k]) => !pluginSlots.includes(k))),
|
|
399
|
+
}
|
|
400
|
+
: config));
|
|
268
401
|
const assemblies = (Array.isArray(config.assemblies)
|
|
269
402
|
? config.assemblies
|
|
270
403
|
: isRecord(config.assembly)
|
|
@@ -280,6 +413,7 @@ export function validateConfig(config, manifest = configManifest) {
|
|
|
280
413
|
.filter((n) => typeof n === 'string')),
|
|
281
414
|
assemblyCount: assemblies.length,
|
|
282
415
|
seenTrackIds: new Set(),
|
|
416
|
+
declaresPlugins,
|
|
283
417
|
// Both the written trackId and the one the `{name, uri}` shorthand derives,
|
|
284
418
|
// since a config using the shorthand still gets the derived id at runtime.
|
|
285
419
|
sequenceTrackIds: new Set(assemblies.flatMap(a => {
|
|
@@ -316,5 +450,6 @@ export function validateConfig(config, manifest = configManifest) {
|
|
|
316
450
|
// Tracks are registered before the session is checked, so a session may
|
|
317
451
|
// reference any track in the file regardless of declaration order.
|
|
318
452
|
checkSession(config.defaultSession, manifest, report, ctx);
|
|
453
|
+
checkMarkDisplays(config, manifest, '', report);
|
|
319
454
|
return report.result();
|
|
320
455
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '5.0.0-beta.
|
|
1
|
+
export const version = '5.0.0-beta.9';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/cli",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A command line tool for working with JBrowse 2",
|
|
6
6
|
"keywords": [
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"ixixx": "^3.0.13",
|
|
33
33
|
"serve-handler": "^6.1.7",
|
|
34
34
|
"tmp": "^0.2.7",
|
|
35
|
-
"@jbrowse/add-track-core": "^5.0.0-beta.
|
|
36
|
-
"@jbrowse/
|
|
37
|
-
"@jbrowse/
|
|
35
|
+
"@jbrowse/add-track-core": "^5.0.0-beta.9",
|
|
36
|
+
"@jbrowse/text-indexing-core": "^5.0.0-beta.9",
|
|
37
|
+
"@jbrowse/cigar-utils": "^5.0.0-beta.9"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|