@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.
- package/README.md +451 -72
- package/dist/base.js +0 -3
- package/dist/bin.js +2 -1
- package/dist/cliFetch.js +3 -0
- package/dist/commands/add-assembly/index.js +46 -19
- package/dist/commands/add-assembly/utils.js +204 -290
- package/dist/commands/add-connection.js +73 -79
- package/dist/commands/add-track-json.js +24 -12
- package/dist/commands/add-track-utils/adapter-utils.js +189 -288
- package/dist/commands/add-track-utils/file-operations.js +27 -11
- package/dist/commands/add-track-utils/multiwig.js +47 -0
- package/dist/commands/add-track-utils/track-config.js +66 -25
- package/dist/commands/add-track-utils/validators.js +36 -20
- package/dist/commands/add-track.js +200 -62
- package/dist/commands/admin-server/index.js +39 -50
- package/dist/commands/admin-server/utils.js +238 -126
- package/dist/commands/create.js +18 -34
- package/dist/commands/make-density/density-generator.js +180 -0
- package/dist/commands/make-density/index.js +139 -0
- package/dist/commands/make-pif/index.js +133 -11
- package/dist/commands/make-pif/pif-generator.js +243 -43
- package/dist/commands/process-utils.js +13 -2
- package/dist/commands/remove-track.js +20 -13
- package/dist/commands/set-default-session.js +35 -31
- package/dist/commands/shared/config-operations.js +3 -10
- package/dist/commands/shared/density.js +12 -0
- package/dist/commands/shared/sort-utils.js +67 -10
- package/dist/commands/shared/validators.js +38 -3
- package/dist/commands/sort-bed.js +2 -33
- package/dist/commands/sort-gff.js +2 -33
- package/dist/commands/text-index/adapter-utils.js +19 -26
- package/dist/commands/text-index/aggregate.js +40 -34
- package/dist/commands/text-index/command.js +37 -7
- package/dist/commands/text-index/config-utils.js +62 -85
- package/dist/commands/text-index/file-list.js +32 -18
- package/dist/commands/text-index/index.js +4 -4
- package/dist/commands/text-index/indexing-utils.js +44 -96
- package/dist/commands/text-index/per-track.js +44 -31
- package/dist/commands/text-index/validators.js +1 -1
- package/dist/commands/upgrade.js +20 -32
- package/dist/commands/validate/configManifest.generated.js +7613 -0
- package/dist/commands/validate/configSchema.generated.js +20400 -0
- package/dist/commands/validate/displayDefaultKeys.js +28 -0
- package/dist/commands/validate/index.js +113 -0
- package/dist/commands/validate/liftConfig.js +34 -0
- package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
- package/dist/commands/validate/markRules/autoBin.js +27 -0
- package/dist/commands/validate/markRules/colorScale.js +244 -0
- package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
- package/dist/commands/validate/markRules/jexlStrings.js +23 -0
- package/dist/commands/validate/markRules/markProblems.js +532 -0
- package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
- package/dist/commands/validate/markRules/markSpecs.js +96 -0
- package/dist/commands/validate/markRules/markVocabulary.js +57 -0
- package/dist/commands/validate/markRules/shapeNameList.js +7 -0
- package/dist/commands/validate/markRules/stepChannels.js +55 -0
- package/dist/commands/validate/schemaValidate.js +425 -0
- package/dist/commands/validate/suggest.js +35 -0
- package/dist/commands/validate/types.js +6 -0
- package/dist/commands/validate/validateConfig.js +502 -0
- package/dist/index.js +104 -50
- package/dist/types/common.js +3 -27
- package/dist/utils.js +309 -128
- package/dist/version.js +1 -1
- package/package.json +8 -14
- package/bin/run +0 -4
- package/bundle/index.js +0 -3686
- package/dist/commands/make-pif/cigar-utils.js +0 -24
- package/dist/commands/make-pif/file-utils.js +0 -30
- package/dist/commands/track-utils.js +0 -74
- package/dist/util.js +0 -35
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markVocabulary.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
|
+
/**
|
|
3
|
+
* The names a `marks` list is written in and the ones a slot left off reads
|
|
4
|
+
* as, in a module that imports nothing: the config schema declares its
|
|
5
|
+
* enumerations and defaults from here, the rule list reads the same constants,
|
|
6
|
+
* and `jbrowse validate` carries a copy of both.
|
|
7
|
+
*/
|
|
8
|
+
export const MARK_TYPES = [
|
|
9
|
+
'bar',
|
|
10
|
+
'point',
|
|
11
|
+
'rule',
|
|
12
|
+
'line',
|
|
13
|
+
'span',
|
|
14
|
+
'text',
|
|
15
|
+
'link',
|
|
16
|
+
];
|
|
17
|
+
export const DEFAULT_MARK_TYPE = 'bar';
|
|
18
|
+
/** How a line joins its values: held across each span, or centre to centre. */
|
|
19
|
+
export const LINE_INTERPOLATIONS = ['step', 'linear'];
|
|
20
|
+
export const DEFAULT_LINE_INTERPOLATE = 'step';
|
|
21
|
+
/** The width a line strokes at where its `size` is left unwritten. */
|
|
22
|
+
export const DEFAULT_LINE_WIDTH_PX = 1;
|
|
23
|
+
export const LINK_SHAPES = ['dome', 'arc', 'line'];
|
|
24
|
+
export const DEFAULT_LINK_SHAPE = 'dome';
|
|
25
|
+
/** The stroke a link draws at where its `size` is left unwritten. */
|
|
26
|
+
export const DEFAULT_LINK_STROKE_PX = 2;
|
|
27
|
+
/** The fraction of its row a span fills where its `rowProportion` is left unwritten. */
|
|
28
|
+
export const DEFAULT_ROW_PROPORTION = 1;
|
|
29
|
+
export const SIZE_SCALES = ['linear', 'log'];
|
|
30
|
+
export const MARK_SOURCES = ['features', 'density'];
|
|
31
|
+
export const DEFAULT_MARK_SOURCE = 'features';
|
|
32
|
+
export const DEFAULT_FORMULA_AS = 'value';
|
|
33
|
+
export const DEFAULT_BIN_FIELD = 'start';
|
|
34
|
+
export const DEFAULT_BIN_AS = ['start', 'end'];
|
|
35
|
+
/** What a `bin` over `fields` writes on each piece: the bases of its interval inside its bin. */
|
|
36
|
+
export const BIN_OVERLAP_FIELD = 'overlap';
|
|
37
|
+
export const DEFAULT_COVERAGE_AS = 'coverage';
|
|
38
|
+
export const DEFAULT_FLATTEN_FIELD = 'subfeatures';
|
|
39
|
+
export const DEFAULT_CELLS_FIELD = 'seq';
|
|
40
|
+
/** What a `cells` step writes: `state` on every cell, `base` on a mismatch or an insertion, `match` off a gap, `length` on an insertion. */
|
|
41
|
+
export const CELLS_FIELDS = ['state', 'base', 'match', 'length'];
|
|
42
|
+
export const DEFAULT_PILEUP_AS = 'row';
|
|
43
|
+
export const DEFAULT_PILEUP_FIELDS = ['start', 'end'];
|
|
44
|
+
export const DEFAULT_TEXT_FIELD = 'name';
|
|
45
|
+
export const DEFAULT_X2 = 'end';
|
|
46
|
+
/** What a `mate` step writes on every feature it answers. */
|
|
47
|
+
export const MATE_FIELDS = [
|
|
48
|
+
'mate.refName',
|
|
49
|
+
'mate.start',
|
|
50
|
+
'mate.end',
|
|
51
|
+
'mate.mateDirection',
|
|
52
|
+
'mateDirection',
|
|
53
|
+
'alt',
|
|
54
|
+
'svType',
|
|
55
|
+
];
|
|
56
|
+
export const AGGREGATE_OPS = ['count', 'sum', 'mean', 'min', 'max'];
|
|
57
|
+
export const DEFAULT_AGGREGATE_OP = 'count';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Generated by scripts/generateMarkRules.ts from packages/core/src/util/shapeNameList.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
|
+
/** The shape names in range order: what an unlisted `range` hands out. */
|
|
3
|
+
export const SHAPE_NAMES = [
|
|
4
|
+
'circle',
|
|
5
|
+
'triangle-down',
|
|
6
|
+
'diamond',
|
|
7
|
+
];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/stepChannels.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
|
+
import { aggregateFieldName } from './markRuleFacts.js';
|
|
3
|
+
import { DEFAULT_AGGREGATE_OP, DEFAULT_COVERAGE_AS, DEFAULT_PILEUP_AS, } from './markVocabulary.js';
|
|
4
|
+
/** The far end a `mate` step writes, as `x2` reads a position on another sequence. */
|
|
5
|
+
export const MATE_X2 = { chrom: 'mate.refName', pos: 'mate.start' };
|
|
6
|
+
function outputOf(step, fallback) {
|
|
7
|
+
return typeof step.as === 'string' && step.as !== '' ? step.as : fallback;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* What {@link StepChannels} a mark's steps leave it, read over the display's,
|
|
11
|
+
* the facet's and then the mark's own steps in the order they run. An
|
|
12
|
+
* `aggregate` or `coverage` makes its features anew, so it clears the row and
|
|
13
|
+
* the other end a step before it wrote, and an aggregate writing two
|
|
14
|
+
* summaries names no `y`.
|
|
15
|
+
*/
|
|
16
|
+
export function stepChannels(steps) {
|
|
17
|
+
let channels = {};
|
|
18
|
+
for (const step of steps) {
|
|
19
|
+
switch (step?.type) {
|
|
20
|
+
case undefined: {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case 'coverage': {
|
|
24
|
+
channels = { y: outputOf(step, DEFAULT_COVERAGE_AS) };
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case 'aggregate': {
|
|
28
|
+
const [only, ...rest] = step.ops ?? [];
|
|
29
|
+
channels =
|
|
30
|
+
only && rest.length === 0
|
|
31
|
+
? {
|
|
32
|
+
y: aggregateFieldName({
|
|
33
|
+
op: only.op ?? DEFAULT_AGGREGATE_OP,
|
|
34
|
+
field: only.field,
|
|
35
|
+
as: only.as,
|
|
36
|
+
}),
|
|
37
|
+
}
|
|
38
|
+
: {};
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'pileup': {
|
|
42
|
+
channels = { ...channels, row: outputOf(step, DEFAULT_PILEUP_AS) };
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
case 'mate': {
|
|
46
|
+
channels = { ...channels, x2: MATE_X2 };
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
default: {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return channels;
|
|
55
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// Validates a config against the generated JSON Schema and turns each failure
|
|
2
|
+
// into a Problem an author can act on: the path in the config's own spelling,
|
|
3
|
+
// the accepted keys with a did-you-mean, and for a union the errors of the
|
|
4
|
+
// branch the value was written for rather than of every branch.
|
|
5
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
6
|
+
import { configJsonSchema } from './configSchema.generated.js';
|
|
7
|
+
import { didYouMean } from './suggest.js';
|
|
8
|
+
const root = configJsonSchema;
|
|
9
|
+
const defs = root.$defs;
|
|
10
|
+
const schemaId = root.$id;
|
|
11
|
+
// Every object inside the schema, keyed by identity, to its JSON pointer: ajv
|
|
12
|
+
// reports `parentSchema` as the object and its `schemaPath` relative to the
|
|
13
|
+
// nearest $ref target, and a union branch is re-validated by absolute pointer.
|
|
14
|
+
const pointers = new Map();
|
|
15
|
+
function index(node, pointer) {
|
|
16
|
+
if (typeof node !== 'object' || node === null) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
pointers.set(node, pointer);
|
|
20
|
+
for (const [key, value] of Object.entries(node)) {
|
|
21
|
+
index(value, `${pointer}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
index(root, '');
|
|
25
|
+
let ajv;
|
|
26
|
+
function validatorFor(pointer) {
|
|
27
|
+
// No meta-validation and no codegen optimization: the schema is generated
|
|
28
|
+
// and its own test checks it against the draft, and a config is validated
|
|
29
|
+
// once per process, so the compile is the cost that matters.
|
|
30
|
+
ajv ??= new Ajv2020({
|
|
31
|
+
allErrors: true,
|
|
32
|
+
strict: false,
|
|
33
|
+
verbose: true,
|
|
34
|
+
validateSchema: false,
|
|
35
|
+
code: { optimize: 0 },
|
|
36
|
+
});
|
|
37
|
+
const whole = ajv.getSchema(schemaId) ?? ajv.compile(root);
|
|
38
|
+
if (pointer === '') {
|
|
39
|
+
return whole;
|
|
40
|
+
}
|
|
41
|
+
const fn = ajv.getSchema(`${schemaId}#${pointer}`);
|
|
42
|
+
if (!fn) {
|
|
43
|
+
throw new Error(`no schema at ${pointer}`);
|
|
44
|
+
}
|
|
45
|
+
return fn;
|
|
46
|
+
}
|
|
47
|
+
function refName(schema) {
|
|
48
|
+
return typeof schema?.$ref === 'string'
|
|
49
|
+
? schema.$ref.replace('#/$defs/', '')
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
function resolve(schema) {
|
|
53
|
+
const name = refName(schema);
|
|
54
|
+
return name ? (defs[name] ?? schema) : schema;
|
|
55
|
+
}
|
|
56
|
+
// The keys an object schema accepts: its own properties plus those of every
|
|
57
|
+
// slot table it intersects through allOf.
|
|
58
|
+
function acceptedKeys(schema) {
|
|
59
|
+
const own = Object.keys(schema.properties ?? {});
|
|
60
|
+
const composed = (schema.allOf ?? []).flatMap(branch => Object.keys(resolve(branch).properties ?? {}));
|
|
61
|
+
return [...new Set([...own, ...composed])];
|
|
62
|
+
}
|
|
63
|
+
function describe(schema) {
|
|
64
|
+
const name = refName(schema);
|
|
65
|
+
if (name === 'FileLocation') {
|
|
66
|
+
return 'a file location ({ "uri": ... } or { "localPath": ... })';
|
|
67
|
+
}
|
|
68
|
+
if (name === 'JexlString') {
|
|
69
|
+
return 'a "jexl:" expression';
|
|
70
|
+
}
|
|
71
|
+
if (name === 'CssColor') {
|
|
72
|
+
return 'a CSS color (a name like "red", "#rrggbb", "rgb()" or "hsl()")';
|
|
73
|
+
}
|
|
74
|
+
if (name) {
|
|
75
|
+
return `a ${name}`;
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(schema.enum)) {
|
|
78
|
+
return `one of ${schema.enum.map(v => JSON.stringify(v)).join(', ')}`;
|
|
79
|
+
}
|
|
80
|
+
if ('const' in schema) {
|
|
81
|
+
return JSON.stringify(schema.const);
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(schema.anyOf)) {
|
|
84
|
+
return schema.anyOf.map(describe).join(' or ');
|
|
85
|
+
}
|
|
86
|
+
switch (schema.type) {
|
|
87
|
+
case 'array':
|
|
88
|
+
return schema.items
|
|
89
|
+
? `an array of ${describe(schema.items)}`
|
|
90
|
+
: 'an array';
|
|
91
|
+
case 'object':
|
|
92
|
+
return 'an object';
|
|
93
|
+
case 'string':
|
|
94
|
+
return 'a string';
|
|
95
|
+
case 'number':
|
|
96
|
+
case 'integer':
|
|
97
|
+
return 'a number';
|
|
98
|
+
case 'boolean':
|
|
99
|
+
return 'a boolean';
|
|
100
|
+
default:
|
|
101
|
+
return 'a value';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function pathOf(instancePath) {
|
|
105
|
+
return instancePath
|
|
106
|
+
.split('/')
|
|
107
|
+
.slice(1)
|
|
108
|
+
.map(seg => seg.replaceAll('~1', '/').replaceAll('~0', '~'))
|
|
109
|
+
.map(seg => (/^\d+$/.test(seg) ? `[${seg}]` : `.${seg}`))
|
|
110
|
+
.join('')
|
|
111
|
+
.replace(/^\./, '');
|
|
112
|
+
}
|
|
113
|
+
function join(where, key) {
|
|
114
|
+
return where && key ? `${where}.${key}` : where || key;
|
|
115
|
+
}
|
|
116
|
+
function isRecord(value) {
|
|
117
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
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
|
+
}
|
|
128
|
+
function typeOf(data) {
|
|
129
|
+
return isRecord(data) && typeof data.type === 'string' ? data.type : undefined;
|
|
130
|
+
}
|
|
131
|
+
// Whether an object schema is for this `type`: a def names it as a const or an
|
|
132
|
+
// alias enum, a dispatching union lists it in its hint.
|
|
133
|
+
function admitsType(schema, type) {
|
|
134
|
+
const prop = schema.properties?.type;
|
|
135
|
+
const hint = prop?.anyOf?.[0];
|
|
136
|
+
return [prop, hint].some(p => p &&
|
|
137
|
+
(p.const === type || (Array.isArray(p.enum) && p.enum.includes(type))));
|
|
138
|
+
}
|
|
139
|
+
const viewNames = new Set(defs.View.properties.type.anyOf[0]
|
|
140
|
+
.enum);
|
|
141
|
+
// What the message says an unknown key is, by the kind of object it sits on.
|
|
142
|
+
// A session node is instantiated by a state model rather than a config schema,
|
|
143
|
+
// so a slot name written there is dropped just like a misspelling — and the
|
|
144
|
+
// advice differs, because the key is real and there are two right places for
|
|
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
|
+
function unknownKeyMessage(key, schema, accepted) {
|
|
152
|
+
const title = typeof schema.title === 'string' ? schema.title : '';
|
|
153
|
+
const guess = didYouMean(key, accepted);
|
|
154
|
+
if (key === 'sequenceAdapter' && !title.endsWith('Snapshot')) {
|
|
155
|
+
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';
|
|
156
|
+
}
|
|
157
|
+
if (title.endsWith('Snapshot')) {
|
|
158
|
+
const slots = Object.keys(defs[`${title.replace(/Snapshot$/, '')}Slots`]?.properties);
|
|
159
|
+
return slots.includes(key)
|
|
160
|
+
? `"${key}" is a config slot, not a display property, so a session snapshot drops it and the setting silently does nothing — put it on the track's "displays" entry under "tracks", or in a "trackConfigDeltas" entry, instead`
|
|
161
|
+
: `unknown display property "${key}"${didYouMean(key, [...accepted, ...slots])} — a session snapshot drops keys the display does not declare, so this setting silently does nothing`;
|
|
162
|
+
}
|
|
163
|
+
if (viewNames.has(title)) {
|
|
164
|
+
const elsewhere = [...viewNames].filter(name => name !== title && acceptedKeys(defs[name]).includes(key));
|
|
165
|
+
return elsewhere.length
|
|
166
|
+
? `"${key}" is a setting of ${elsewhere.join(', ')}, not of ${title}${guess} — a session snapshot drops keys the view does not declare, so this setting silently does nothing`
|
|
167
|
+
: `unknown view key "${key}"${guess} — a session snapshot drops keys the view does not declare, so this setting silently does nothing`;
|
|
168
|
+
}
|
|
169
|
+
if (title === 'Session') {
|
|
170
|
+
return `unknown session key "${key}"${guess} — a session snapshot drops keys it does not declare, so this setting silently does nothing`;
|
|
171
|
+
}
|
|
172
|
+
if (title.endsWith('DisplayDefaults')) {
|
|
173
|
+
return `no display of this track declares "${key}"${guess}`;
|
|
174
|
+
}
|
|
175
|
+
if (title.endsWith('TrackEntry')) {
|
|
176
|
+
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`;
|
|
177
|
+
}
|
|
178
|
+
if (schema['x-closed'] === true) {
|
|
179
|
+
return `unknown slot "${key}"${guess} — ${title} takes ${listed(accepted)}, and JBrowse refuses to load it rather than drop a key it does not declare`;
|
|
180
|
+
}
|
|
181
|
+
return `unknown slot "${key}"${guess} — JBrowse ignores keys it does not declare, so this setting silently does nothing`;
|
|
182
|
+
}
|
|
183
|
+
function isJexl(value) {
|
|
184
|
+
return typeof value === 'string' && value.startsWith('jexl:');
|
|
185
|
+
}
|
|
186
|
+
// The keywords a slot declaring no contextVariable fails a `jexl:` string on:
|
|
187
|
+
// a plain string's `not`, a number's `type`, a color's `pattern`, an enum's
|
|
188
|
+
// own, and a union of those
|
|
189
|
+
const REFUSES_CALLBACK = new Set([
|
|
190
|
+
'not',
|
|
191
|
+
'type',
|
|
192
|
+
'pattern',
|
|
193
|
+
'enum',
|
|
194
|
+
'const',
|
|
195
|
+
'anyOf',
|
|
196
|
+
]);
|
|
197
|
+
function refusedCallback(value) {
|
|
198
|
+
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`;
|
|
199
|
+
}
|
|
200
|
+
function isScalar(schema) {
|
|
201
|
+
const resolved = resolve(schema);
|
|
202
|
+
return (resolved.properties === undefined &&
|
|
203
|
+
resolved.items === undefined &&
|
|
204
|
+
resolved.allOf === undefined &&
|
|
205
|
+
resolved.type !== 'object' &&
|
|
206
|
+
resolved.type !== 'array');
|
|
207
|
+
}
|
|
208
|
+
function isJexlBranch(schema) {
|
|
209
|
+
return refName(schema) === 'JexlString';
|
|
210
|
+
}
|
|
211
|
+
function branchesOf(error) {
|
|
212
|
+
return error.parentSchema.anyOf;
|
|
213
|
+
}
|
|
214
|
+
// Which branch of a union the value was written for: the one whose `type` it
|
|
215
|
+
// names, else the one it comes closest to satisfying.
|
|
216
|
+
function pickBranch(error, data, errors) {
|
|
217
|
+
const branches = branchesOf(error);
|
|
218
|
+
const type = typeOf(data);
|
|
219
|
+
if (type !== undefined) {
|
|
220
|
+
const byType = branches.findIndex(b => admitsType(resolve(b), type));
|
|
221
|
+
if (byType >= 0) {
|
|
222
|
+
return byType;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const jsonType = jsonTypeOf(data);
|
|
226
|
+
const byJsonType = branches.flatMap((b, i) => (b.type ?? resolve(b).type) === jsonType ? [i] : []);
|
|
227
|
+
if (byJsonType.length === 1) {
|
|
228
|
+
return byJsonType[0];
|
|
229
|
+
}
|
|
230
|
+
if (isRecord(data)) {
|
|
231
|
+
const tagged = branches.findIndex(b => 'locationType' in data &&
|
|
232
|
+
admitsLocation(resolve(b), data.locationType));
|
|
233
|
+
if (tagged >= 0) {
|
|
234
|
+
return tagged;
|
|
235
|
+
}
|
|
236
|
+
const untagged = branches.findIndex(b => 'not' in resolve(b));
|
|
237
|
+
if (untagged >= 0 && !('locationType' in data)) {
|
|
238
|
+
return untagged;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const counts = branches.map((_, i) => {
|
|
242
|
+
const prefix = `${error.schemaPath}/${i}/`;
|
|
243
|
+
return errors.filter(e => e.schemaPath.startsWith(prefix) &&
|
|
244
|
+
e.instancePath.startsWith(error.instancePath)).length;
|
|
245
|
+
});
|
|
246
|
+
const structural = branches.map((b, i) => counts[i] === 0 ? Infinity : counts[i]);
|
|
247
|
+
return structural.indexOf(Math.min(...structural));
|
|
248
|
+
}
|
|
249
|
+
function admitsLocation(schema, locationType) {
|
|
250
|
+
const prop = schema.properties?.locationType;
|
|
251
|
+
return prop?.const === locationType;
|
|
252
|
+
}
|
|
253
|
+
// A schema's `requires` entry, which the generator writes as an `if`/`then`
|
|
254
|
+
// annotated with the entry's id, the slot path it requires and its message.
|
|
255
|
+
function requirementOf(error) {
|
|
256
|
+
const parent = error.parentSchema ?? {};
|
|
257
|
+
const stated = parent['x-requirement'];
|
|
258
|
+
return error.keyword === 'if' && isRecord(stated)
|
|
259
|
+
? {
|
|
260
|
+
id: String(stated.id),
|
|
261
|
+
slot: String(stated.slot),
|
|
262
|
+
message: String(parent.errorMessage),
|
|
263
|
+
branch: `${error.schemaPath.replace(/\/if$/, '')}/then/`,
|
|
264
|
+
}
|
|
265
|
+
: undefined;
|
|
266
|
+
}
|
|
267
|
+
// Whether a failure is one requirement's: the `if` itself, or an error under
|
|
268
|
+
// its `then` at or beneath the object the `if` judged.
|
|
269
|
+
function statesRequirement(error, errors) {
|
|
270
|
+
return errors.some(other => {
|
|
271
|
+
const requirement = requirementOf(other);
|
|
272
|
+
return (requirement !== undefined &&
|
|
273
|
+
(other === error ||
|
|
274
|
+
(error.schemaPath.startsWith(requirement.branch) &&
|
|
275
|
+
error.instancePath.startsWith(other.instancePath))));
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function requiredOnly(branches) {
|
|
279
|
+
return branches.every(b => Object.keys(b).length === 1 && Array.isArray(b.required));
|
|
280
|
+
}
|
|
281
|
+
// Converts ajv's errors at one validation into Problems, recursing into the
|
|
282
|
+
// chosen branch of each failed union so the author sees one diagnosis rather
|
|
283
|
+
// than one per alternative.
|
|
284
|
+
function explain(data, errors, problems, depth) {
|
|
285
|
+
const covered = new Set();
|
|
286
|
+
const emit = (where, message, rule) => {
|
|
287
|
+
const key = `${where}\0${message}`;
|
|
288
|
+
if (!covered.has(key)) {
|
|
289
|
+
covered.add(key);
|
|
290
|
+
problems.push({
|
|
291
|
+
level: 'error',
|
|
292
|
+
where,
|
|
293
|
+
message,
|
|
294
|
+
...(rule ? { rule } : {}),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
// A union failure explains everything beneath it; the branch is re-validated
|
|
299
|
+
// on its own so the sub-errors reported for the other branches drop out.
|
|
300
|
+
// A union nested as another's branch at the same path (`number | "auto"`
|
|
301
|
+
// inside a slot's `value | jexl:`) is the outer one's to explain, and never
|
|
302
|
+
// the other way round, or neither is reported.
|
|
303
|
+
const unions = errors.filter(e => e.keyword === 'anyOf');
|
|
304
|
+
const isBranchOf = (u, e) => branchesOf(u).some(b => resolve(b) === e.parentSchema);
|
|
305
|
+
const shadowed = (e) => unions.some(u => u !== e &&
|
|
306
|
+
(e.instancePath.startsWith(`${u.instancePath}/`) ||
|
|
307
|
+
(e.instancePath === u.instancePath &&
|
|
308
|
+
!(e.keyword === 'anyOf' && isBranchOf(e, u)))));
|
|
309
|
+
for (const error of errors) {
|
|
310
|
+
if (shadowed(error)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const where = pathOf(error.instancePath);
|
|
314
|
+
const requirement = requirementOf(error);
|
|
315
|
+
if (requirement) {
|
|
316
|
+
emit(join(where, requirement.slot), requirement.message, requirement.id);
|
|
317
|
+
}
|
|
318
|
+
if (error.keyword === 'if' || statesRequirement(error, errors)) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const parent = error.parentSchema;
|
|
322
|
+
if (isJexl(error.data) &&
|
|
323
|
+
REFUSES_CALLBACK.has(error.keyword) &&
|
|
324
|
+
!(error.keyword === 'anyOf' && branchesOf(error).some(isJexlBranch))) {
|
|
325
|
+
emit(where, refusedCallback(error.data));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
switch (error.keyword) {
|
|
329
|
+
case 'additionalProperties':
|
|
330
|
+
case 'unevaluatedProperties': {
|
|
331
|
+
const key = String(error.params.additionalProperty ?? error.params.unevaluatedProperty);
|
|
332
|
+
emit(join(where, key), unknownKeyMessage(key, parent, acceptedKeys(parent)));
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case 'required':
|
|
336
|
+
emit(where, `missing "${String(error.params.missingProperty)}"`);
|
|
337
|
+
break;
|
|
338
|
+
case 'anyOf': {
|
|
339
|
+
const branches = branchesOf(error);
|
|
340
|
+
const value = error.data;
|
|
341
|
+
if (requiredOnly(branches)) {
|
|
342
|
+
emit(where, `needs one of ${branches.map(b => `"${b.required[0]}"`).join(', ')}`);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
const jexlBranch = branches.findIndex(isJexlBranch);
|
|
346
|
+
if (jexlBranch >= 0 && !isJexl(value)) {
|
|
347
|
+
const others = branches.filter((_, i) => i !== jexlBranch);
|
|
348
|
+
emit(where, `expected ${others.map(describe).join(' or ')} or a "jexl:" expression, got ${JSON.stringify(value)}`);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (!isRecord(value) &&
|
|
352
|
+
!Array.isArray(value) &&
|
|
353
|
+
branches.every(isScalar)) {
|
|
354
|
+
emit(where, `expected ${branches.map(describe).join(' or ')}, got ${JSON.stringify(value)}`);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
if (depth > 8) {
|
|
358
|
+
emit(where, `does not match any accepted form`);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
const pointer = pointers.get(parent);
|
|
362
|
+
const branch = pickBranch(error, value, errors);
|
|
363
|
+
if (pointer === undefined || branch < 0) {
|
|
364
|
+
emit(where, 'does not match any accepted form');
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
const fn = validatorFor(`${pointer}/anyOf/${branch}`);
|
|
368
|
+
if (!fn(value) && fn.errors?.length) {
|
|
369
|
+
const nested = [];
|
|
370
|
+
explain(value, fn.errors, nested, depth + 1);
|
|
371
|
+
for (const p of nested) {
|
|
372
|
+
emit(join(where, p.where).replaceAll('.[', '['), p.message);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
emit(where, 'does not match any accepted form');
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case 'pattern':
|
|
381
|
+
emit(where, error.schemaPath.endsWith('/CssColor/pattern')
|
|
382
|
+
? `expected ${describe({ $ref: '#/$defs/CssColor' })}, got ${JSON.stringify(error.data)}`
|
|
383
|
+
: `expected a string matching /${String(parent.pattern)}/, got ${JSON.stringify(error.data)}`);
|
|
384
|
+
break;
|
|
385
|
+
case 'type':
|
|
386
|
+
case 'enum':
|
|
387
|
+
case 'const':
|
|
388
|
+
emit(where, `expected ${describe(parent)}, got ${JSON.stringify(error.data)}`);
|
|
389
|
+
break;
|
|
390
|
+
case 'not':
|
|
391
|
+
emit(where, `must not ${describe(parent.not)}`);
|
|
392
|
+
break;
|
|
393
|
+
default:
|
|
394
|
+
emit(where, error.message ?? error.keyword);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// The problems in `data` against the whole schema, or against one of its
|
|
399
|
+
// definitions by pointer (`/$defs/View` for a session spec's view).
|
|
400
|
+
export function schemaProblems(data, pointer = '') {
|
|
401
|
+
const validate = validatorFor(pointer);
|
|
402
|
+
if (validate(data)) {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
const problems = [];
|
|
406
|
+
explain(data, validate.errors ?? [], problems, 0);
|
|
407
|
+
return problems;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Whether `data` has the keys and value types the definition at `pointer`
|
|
411
|
+
* declares, whatever its `requires` entries say of it: the contract a rule
|
|
412
|
+
* list reads a config under, since a load refuses a key or a type and no
|
|
413
|
+
* combination of slots.
|
|
414
|
+
*/
|
|
415
|
+
export function hasDeclaredShape(data, pointer) {
|
|
416
|
+
const validate = validatorFor(pointer);
|
|
417
|
+
const errors = validate(data) ? [] : (validate.errors ?? []);
|
|
418
|
+
return errors.every(error => statesRequirement(error, errors));
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The keys core declares directly under `configuration`. A plugin adds its own
|
|
422
|
+
* beside them, the way `LinearGenomeViewPlugin` is core's.
|
|
423
|
+
*/
|
|
424
|
+
export const rootConfigurationSlots = new Set(Object.keys(defs.RootConfiguration.properties));
|
|
425
|
+
export { schemaId as configSchemaUrl };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// "did you mean" for a mistyped identifier, shared by the validator and by
|
|
2
|
+
// add-track's up-front check on the settings it is about to write.
|
|
3
|
+
function editDistance(a, b) {
|
|
4
|
+
// Two rolling rows rather than the full matrix; these are short identifiers
|
|
5
|
+
// and this runs once per unknown key.
|
|
6
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
7
|
+
for (let i = 1; i <= a.length; i++) {
|
|
8
|
+
const row = [i];
|
|
9
|
+
for (let j = 1; j <= b.length; j++) {
|
|
10
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
11
|
+
}
|
|
12
|
+
prev = row;
|
|
13
|
+
}
|
|
14
|
+
return prev[b.length];
|
|
15
|
+
}
|
|
16
|
+
// The nearest candidate, if it is near enough to be worth naming. The threshold
|
|
17
|
+
// scales with length so `uri` doesn't match every other three-letter slot while
|
|
18
|
+
// a longer typo still resolves.
|
|
19
|
+
export function suggest(word, candidates) {
|
|
20
|
+
const limit = Math.max(2, Math.floor(word.length / 3));
|
|
21
|
+
let best;
|
|
22
|
+
let bestScore = Infinity;
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
const score = editDistance(word.toLowerCase(), candidate.toLowerCase());
|
|
25
|
+
if (score < bestScore) {
|
|
26
|
+
bestScore = score;
|
|
27
|
+
best = candidate;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return bestScore <= limit ? best : undefined;
|
|
31
|
+
}
|
|
32
|
+
export function didYouMean(word, candidates) {
|
|
33
|
+
const hit = suggest(word, candidates);
|
|
34
|
+
return hit ? ` — did you mean "${hit}"?` : '';
|
|
35
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Shape of the generated config manifest (configManifest.generated.ts), which
|
|
2
|
+
// scripts/generateConfigManifest.ts reads out of the live ConfigurationSchema
|
|
3
|
+
// objects. Declared here rather than inferred from the generated file so the
|
|
4
|
+
// generator and the validator agree on a contract, and a generator change that
|
|
5
|
+
// drops a field is a type error rather than a silent behaviour change.
|
|
6
|
+
export {};
|