@jbrowse/cli 5.0.0-beta.1 → 5.0.0-beta.2

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.
@@ -344,9 +344,101 @@ function checkSessionViewTracks(view, manifest, where, report) {
344
344
  }
345
345
  }
346
346
  }
347
- // A view's `init` names tracks and an assembly by id. Nothing validates those at
348
- // load: a trackId that does not exist simply fails to open, which reads as a
349
- // rendering bug rather than a typo.
347
+ // A view node in a session — `defaultSession.views[]` — is instantiated by the
348
+ // view's STATE MODEL, and a view has no config schema at all, so every setting
349
+ // an author writes lands on that node: the properties MST restores, plus the
350
+ // launch keys a launcher resolves (`assembly`, `loc`, `tracks`). Those two
351
+ // together are the whole accepted set, which is what makes this check
352
+ // exhaustive rather than a guess — anything else is dropped exactly like a
353
+ // misspelling, and the view opens on its defaults with nothing said.
354
+ //
355
+ // The same trap checkSessionDisplay catches one level down, and the same three
356
+ // diagnoses: a real key in the wrong place, a stale spelling that still works,
357
+ // and a key nothing anywhere reads.
358
+ function checkSessionViewKeys(view, entry, manifest, where, report) {
359
+ const accepted = [
360
+ ...entry.stateModelProps,
361
+ ...entry.launchKeys,
362
+ ...(entry.passThrough ?? []),
363
+ ];
364
+ for (const key of Object.keys(view)) {
365
+ // `init` gets its own message from checkSessionView below
366
+ if (accepted.includes(key) || key === 'init') {
367
+ continue;
368
+ }
369
+ // Whichever other view types do read the key. `assembly` on a DotplotView is
370
+ // not a misspelling and no suggestion reaches it, so naming the views that
371
+ // take it is the only thing that tells the author what they wrote.
372
+ const elsewhere = Object.entries(manifest.views)
373
+ .filter(([name, other]) => name !== entry.canonicalName &&
374
+ [...other.stateModelProps, ...other.launchKeys].includes(key))
375
+ .map(([name]) => name);
376
+ report.error(`${where}.${key}`, elsewhere.length
377
+ ? `"${key}" is a setting of ${elsewhere.join(', ')}, not of ${entry.canonicalName}${didYouMean(key, accepted)} — a session snapshot drops keys the view does not declare, so this setting silently does nothing`
378
+ : `unknown view key "${key}"${didYouMean(key, accepted)} — a session snapshot drops keys the view does not declare, so this setting silently does nothing`);
379
+ }
380
+ }
381
+ // The ids a view names, which nothing validates at load: a trackId that does not
382
+ // exist simply fails to open, which reads as a rendering bug rather than a typo.
383
+ function checkViewReferences(view, where, report, ctx) {
384
+ const assembly = view.assembly;
385
+ if (typeof assembly === 'string' && !ctx.assemblyNames.has(assembly)) {
386
+ report.error(`${where}.assembly`, `assembly "${assembly}" is not defined in this config${didYouMean(assembly, [...ctx.assemblyNames])}`);
387
+ }
388
+ if (!Array.isArray(view.tracks)) {
389
+ return;
390
+ }
391
+ // A LinearSyntenyView's `tracks` is one array PER LEVEL (`[[a], [b]]`) — the
392
+ // multi-way form — so flatten one level before reading entries. Treating those
393
+ // inner arrays as track entries reported every genome row of every synteny
394
+ // demo as a track with no trackId.
395
+ const entries = view.tracks.flatMap((t) => Array.isArray(t) ? t : [t]);
396
+ for (const [j, entry] of entries.entries()) {
397
+ // A built track snapshot names its config through `configuration` rather
398
+ // than `trackId`, and is state MST restores rather than a reference to open.
399
+ if (isRecord(entry) && !('trackId' in entry)) {
400
+ continue;
401
+ }
402
+ const trackId = isRecord(entry) ? entry.trackId : entry;
403
+ if (typeof trackId !== 'string') {
404
+ report.error(`${where}.tracks[${j}]`, 'track entry has no trackId');
405
+ }
406
+ else if (!ctx.seenTrackIds.has(trackId) &&
407
+ !ctx.sequenceTrackIds.has(trackId)) {
408
+ report.error(`${where}.tracks[${j}]`, `trackId "${trackId}" is not defined in this config${didYouMean(trackId, [...ctx.seenTrackIds, ...ctx.sequenceTrackIds])}`);
409
+ }
410
+ }
411
+ }
412
+ function checkSessionView(view, manifest, where, report, ctx) {
413
+ const entry = resolveType(view, manifest.views, 'view', `${where}.type`, report);
414
+ // A view type the manifest doesn't know (a plugin's) is already a warning from
415
+ // resolveType, and its keys are unknowable here.
416
+ if (!entry) {
417
+ return;
418
+ }
419
+ checkSessionViewKeys(view, entry, manifest, where, report);
420
+ checkViewReferences(view, where, report, ctx);
421
+ // A warning, not an error: v5 unwraps the nesting on the way in, so the
422
+ // settings do arrive. The keys inside get the same check the flat ones do,
423
+ // since that is where they land.
424
+ if (view.init !== undefined) {
425
+ report.warn(`${where}.init`, 'settings nested under "init", which is deprecated: write every setting directly on the view object');
426
+ if (isRecord(view.init)) {
427
+ checkSessionViewKeys(view.init, entry, manifest, `${where}.init`, report);
428
+ checkViewReferences(view.init, `${where}.init`, report, ctx);
429
+ }
430
+ }
431
+ // A synteny or breakpoint row is a whole view snapshot of its own. Only a row
432
+ // carrying a `type` can be checked: a row written as a recipe names no view
433
+ // type, and which one it becomes is the parent launcher's business.
434
+ if (Array.isArray(view.views)) {
435
+ for (const [i, row] of view.views.entries()) {
436
+ if (isRecord(row) && typeof row.type === 'string') {
437
+ checkSessionView(row, manifest, `${where}.views[${i}]`, report, ctx);
438
+ }
439
+ }
440
+ }
441
+ }
350
442
  function checkSession(session, manifest, report, ctx) {
351
443
  if (!isRecord(session) || !Array.isArray(session.views)) {
352
444
  return;
@@ -355,35 +447,8 @@ function checkSession(session, manifest, report, ctx) {
355
447
  checkSessionViewTracks(view, manifest, `defaultSession.views[${i}]`, report);
356
448
  }
357
449
  for (const [i, view] of session.views.entries()) {
358
- if (!isRecord(view) || !isRecord(view.init)) {
359
- continue;
360
- }
361
- const init = view.init;
362
- const where = `defaultSession.views[${i}].init`;
363
- if (typeof init.assembly === 'string' &&
364
- !ctx.assemblyNames.has(init.assembly)) {
365
- report.error(`${where}.assembly`, `assembly "${init.assembly}" is not defined in this config${didYouMean(init.assembly, [...ctx.assemblyNames])}`);
366
- }
367
- if (Array.isArray(init.tracks)) {
368
- // A LinearSyntenyView's `tracks` is one array PER LEVEL (`[[a], [b]]`) —
369
- // the multi-way form — so flatten one level before reading entries.
370
- // Treating those inner arrays as track entries reported every genome row
371
- // of every synteny demo as a track with no trackId.
372
- const entries = init.tracks.flatMap((t) => Array.isArray(t) ? t : [t]);
373
- for (const [j, entry] of entries.entries()) {
374
- const trackId = typeof entry === 'string'
375
- ? entry
376
- : isRecord(entry)
377
- ? entry.trackId
378
- : undefined;
379
- if (typeof trackId !== 'string') {
380
- report.error(`${where}.tracks[${j}]`, 'track entry has no trackId');
381
- }
382
- else if (!ctx.seenTrackIds.has(trackId) &&
383
- !ctx.sequenceTrackIds.has(trackId)) {
384
- report.error(`${where}.tracks[${j}]`, `trackId "${trackId}" is not defined in this config${didYouMean(trackId, [...ctx.seenTrackIds, ...ctx.sequenceTrackIds])}`);
385
- }
386
- }
450
+ if (isRecord(view)) {
451
+ checkSessionView(view, manifest, `defaultSession.views[${i}]`, report, ctx);
387
452
  }
388
453
  }
389
454
  }
package/dist/index.js CHANGED
@@ -49,6 +49,11 @@ const registry = [
49
49
  summary: 'Creates pairwise indexed PAF (PIF), with bgzip and tabix',
50
50
  load: () => import('./commands/make-pif/index.js').then(m => m.run),
51
51
  },
52
+ {
53
+ name: 'make-density',
54
+ summary: 'Counts feature starts per bin into a bigWig density sidecar, with bedGraphToBigWig',
55
+ load: () => import('./commands/make-density/index.js').then(m => m.run),
56
+ },
52
57
  {
53
58
  name: 'sort-gff',
54
59
  summary: 'Sort a GFF/GTF for tabix: sort -k1,1 -k4,4n, header kept on top',
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '5.0.0-beta.1';
1
+ export const version = '5.0.0-beta.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/cli",
3
- "version": "5.0.0-beta.1",
3
+ "version": "5.0.0-beta.2",
4
4
  "type": "module",
5
5
  "description": "A command line tool for working with JBrowse 2",
6
6
  "keywords": [
@@ -33,9 +33,9 @@
33
33
  "ixixx": "^3.0.13",
34
34
  "serve-handler": "^6.1.7",
35
35
  "tmp": "^0.2.7",
36
- "@jbrowse/add-track-core": "^5.0.0-beta.1",
37
- "@jbrowse/cigar-utils": "^5.0.0-beta.1",
38
- "@jbrowse/text-indexing-core": "^5.0.0-beta.1"
36
+ "@jbrowse/cigar-utils": "^5.0.0-beta.2",
37
+ "@jbrowse/text-indexing-core": "^5.0.0-beta.2",
38
+ "@jbrowse/add-track-core": "^5.0.0-beta.2"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
@@ -1,67 +0,0 @@
1
- // Walk a CIGAR and split the alignment wherever an insertion or deletion is at
2
- // least `splitGap` bp, so each coarse-tier row's bounding box stays tight.
3
- // Returns one segment per contiguous run; `splitGap` must be positive (the
4
- // caller keeps the whole row when there is nothing to split on).
5
- // PAF semantics: qstart < qend always; '-' strand CIGAR walks query backward.
6
- export function splitCigarOnLargeGaps({ cigar, strand, tstart, tend, qstart, qend, splitGap, }) {
7
- const qStep = strand === '-' ? -1 : 1;
8
- let tCursor = tstart;
9
- let qCursor = strand === '-' ? qend : qstart;
10
- let segTStart = tCursor;
11
- let segQAnchor = qCursor;
12
- let segBlockLen = 0;
13
- const out = [];
14
- const emit = () => {
15
- if (tCursor > segTStart || qCursor !== segQAnchor) {
16
- out.push({
17
- tstart: segTStart,
18
- tend: tCursor,
19
- qstart: Math.min(segQAnchor, qCursor),
20
- qend: Math.max(segQAnchor, qCursor),
21
- blockLen: segBlockLen,
22
- });
23
- }
24
- };
25
- // Parse CIGAR directly to avoid intermediate array + string-to-number conversions
26
- let i = 0;
27
- while (i < cigar.length) {
28
- let len = 0;
29
- while (i < cigar.length &&
30
- cigar.charCodeAt(i) >= 48 &&
31
- cigar.charCodeAt(i) <= 57) {
32
- len = len * 10 + cigar.charCodeAt(i++) - 48;
33
- }
34
- const op = cigar[i++];
35
- if ('M=X'.includes(op)) {
36
- tCursor += len;
37
- qCursor += len * qStep;
38
- segBlockLen += len;
39
- }
40
- else if ('DIN'.includes(op)) {
41
- const isLarge = len >= splitGap;
42
- if (isLarge) {
43
- emit();
44
- }
45
- if (op === 'I') {
46
- qCursor += len * qStep;
47
- }
48
- else {
49
- tCursor += len;
50
- }
51
- if (isLarge) {
52
- segTStart = tCursor;
53
- segQAnchor = qCursor;
54
- segBlockLen = 0;
55
- }
56
- else {
57
- segBlockLen += len;
58
- }
59
- }
60
- // S/H/P (clipping/padding) don't consume target or forward-strand query in PAF
61
- }
62
- emit();
63
- return {
64
- segments: out,
65
- closed: tCursor === tend && qCursor === (strand === '-' ? qstart : qend),
66
- };
67
- }