@jbrowse/graph-walks 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,11 +4,13 @@ Cuts a window out of a pangenome graph stored as
4
4
  [gfa-to-tabix](https://github.com/GMOD/gfa-to-tabix) walk files, by range
5
5
  requests.
6
6
 
7
- - **In:** a `gfa-to-tabix --walks` file set and a region on its reference
7
+ - **In:** a `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz` with
8
+ its `.tbi`, and a region on its reference
8
9
  - **Out:** the reference over the region, every haplotype's walk through it, and
9
10
  the nodes and links they visit, as typed arrays
10
- - Reads format 1 (`.walks.bed.gz` with `.nodes.bed.gz` and `.links.bed.gz`) and
11
- format 2 (`.walks.bed.gz` alone), each with its `.tbi`
11
+ - Reads walk format 2, which gfa-to-tabix writes from 1.0. The reader refuses a
12
+ format 1 file, from gfa-to-tabix 0.12 or earlier, with a `WalkFormatError`
13
+ asking for a rebuild
12
14
  - Has no runtime dependencies: it reads through any object with
13
15
  [@gmod/tabix](https://github.com/GMOD/tabix-js)'s `TabixIndexedFile` methods
14
16
 
@@ -25,29 +27,22 @@ npm install @jbrowse/graph-walks @gmod/tabix
25
27
 
26
28
  ```ts
27
29
  import { TabixIndexedFile } from '@gmod/tabix'
28
- import {
29
- WalkReader,
30
- panSNHaplotype,
31
- walkNameFilter,
32
- } from '@jbrowse/graph-walks'
30
+ import { WalkReader, walksKept } from '@jbrowse/graph-walks'
33
31
  import { RemoteFile } from 'generic-filehandle2'
34
32
 
35
- const file = (url: string) =>
33
+ const url = 'https://example.org/hprc-v2.1-mc-grch38.GRCh38.walks.bed.gz'
34
+ const reader = new WalkReader(
36
35
  new TabixIndexedFile({
37
36
  filehandle: new RemoteFile(url),
38
37
  tbiFilehandle: new RemoteFile(`${url}.tbi`),
39
- })
40
- const prefix = 'https://jbrowse.org/demos/hprc/hprc-v2.1-mc-grch38.GRCh38'
41
- const reader = new WalkReader({
42
- walks: file(`${prefix}.walks.bed.gz`),
43
- nodes: file(`${prefix}.nodes.bed.gz`),
44
- })
38
+ }),
39
+ )
45
40
 
46
41
  const refName = 'GRCh38#0#chr6'
47
42
  const { graph, kept, fragments } = await reader.cut(
48
43
  refName,
49
44
  { start: 160_614_798, end: 160_647_758 },
50
- { keep: walkNameFilter(['HG00097', 'HG00133'], panSNHaplotype(refName)) },
45
+ { keep: walksKept(['HG00097', 'HG00133'], refName) },
51
46
  )
52
47
  const tables = graph.tables(kept, fragments)
53
48
  ```
@@ -68,18 +63,35 @@ const tables = graph.tables(kept, fragments)
68
63
  | `context` | bp of reference kept past each end of the region; default 1000 |
69
64
  | `stepBudget` | The most steps a cut keeps; no limit by default |
70
65
  | `byteBudget` | The most compressed bytes a cut fetches; no limit by default |
71
- | `links` | `false` leaves the link file unread |
72
- | `sequences` | Keep each node's bases in `graph.sequences`, from `--sequences` files |
66
+ | `links` | `false` lists no links, for a caller that draws no edges |
67
+ | `sequences` | Keep each node's bases in `graph.sequences`, from a `--sequences` file |
73
68
  | `signal` | An `AbortSignal` that stops the reads |
74
69
 
75
70
  A cut over a budget throws a `RegionTooLargeError`. Its `fitsBp` holds about how
76
71
  wide a window would fit, and JBrowse shows it as a zoom-in notice.
72
+ `WALK_BUDGETS` holds the step and byte budgets the JBrowse plugin and BandageJS
73
+ cut by.
74
+
75
+ ## Naming the window
76
+
77
+ - `reader.sequenceFor(sample, refName)` gives the walk file's sequence for a
78
+ contig on a sample: `chr6` on `GRCh38` is `GRCh38#0#chr6`, and a pggb file's
79
+ `chm13#chr1` is `chr1` on `chm13`
80
+ - `walksKept(haplotypes, refName)` is the `keep` filter for a cut on `refName`:
81
+ its own walk and those of the haplotypes named
82
+ - `refNameLookup` and `resolveRefName` resolve a contig the same way against any
83
+ tabix file's sequence names
77
84
 
78
85
  ## How a cut works
79
86
 
87
+ - A chunk holds the reference's path through it once, each way a haplotype
88
+ leaves that path once, and every other path's piece as the list of ways it
89
+ takes; gfa-to-tabix's
90
+ [walks-format.md](https://github.com/GMOD/gfa-to-tabix/blob/main/docs/walks-format.md)
91
+ describes the rows
80
92
  - A cut reads every chunk under the region, and the chunks before it as far as
81
93
  the header's `maxnode` reaches, since a node is filed under the chunk holding
82
- its start
94
+ its start, and decodes each chunk once
83
95
  - A haplotype that leaves the reference near the region is followed to where it
84
96
  rejoins, and the reference is cut out as far as those rejoins reach
85
97
  - A walk the read cuts off goes on in the chunk its row names (`pv:`/`nx:`
package/esm/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, decodeSteps, joinPieces, keptStepsError, lookbackChunks, parseWalkRow, stepBudgetError, walkCut, walkHeader, walkNameFilter, walkRowName, } from './walkRows.ts';
1
+ export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, walksKept, } from './walkRows.ts';
2
2
  export type { BudgetNames, WalkChunk, WalkFragment, WalkHeader, WalkNode, WalkRow, WalkRun, WalkWindow, } from './walkRows.ts';
3
- export { WalkReader } from './walkReader.ts';
4
- export { referenceNodes } from './walkFormat2.ts';
5
- export type { TabixSource, WalkCutOptions, WalkFileSet, WalkReaderHeader, } from './walkReader.ts';
6
- export { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from './util.ts';
3
+ export { WALK_BUDGETS, WalkFormatError, WalkReader, checkRange, } from './walkReader.ts';
4
+ export { refNameLookup, resolveRefName } from './refNames.ts';
5
+ export { referenceNodes } from './walkChunk.ts';
6
+ export type { TabixSource, WalkCutOptions, WalkReaderHeader, } from './walkReader.ts';
7
+ export { RegionTooLargeError, bpString, panSNContig, panSNHaplotype, panSNMatchesPrefix, panSNSample, } from './util.ts';
7
8
  export type { GraphTables } from './util.ts';
package/esm/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, decodeSteps, joinPieces, keptStepsError, lookbackChunks, parseWalkRow, stepBudgetError, walkCut, walkHeader, walkNameFilter, walkRowName, } from "./walkRows.js";
2
- export { WalkReader } from "./walkReader.js";
3
- export { referenceNodes } from "./walkFormat2.js";
4
- export { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from "./util.js";
1
+ export { BUDGET_NAMES, WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, walkNameFilter, walksKept, } from "./walkRows.js";
2
+ export { WALK_BUDGETS, WalkFormatError, WalkReader, checkRange, } from "./walkReader.js";
3
+ export { refNameLookup, resolveRefName } from "./refNames.js";
4
+ export { referenceNodes } from "./walkChunk.js";
5
+ export { RegionTooLargeError, bpString, panSNContig, panSNHaplotype, panSNMatchesPrefix, panSNSample, } from "./util.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A file's sequence names under each spelling a caller may ask by: the file's
3
+ * own (`GRCh38#0#chr6`), and a PanSN name's contig under its sample (`chr6`
4
+ * on `GRCh38`) and under its haplotype (`chr6` on `GRCh38#0`). A pggb name
5
+ * with no haplotype field, `chm13#chr1`, is `chr1` on `chm13`.
6
+ */
7
+ export declare function refNameLookup(names: string[]): Map<string, string>;
8
+ /**
9
+ * The file's name for `refName` on `assemblyName`, a sample or a haplotype,
10
+ * else for `refName` as the file spells it; undefined when it holds neither
11
+ */
12
+ export declare function resolveRefName(lookup: Map<string, string>, assemblyName: string, refName: string): string | undefined;
@@ -0,0 +1,35 @@
1
+ import { panSNContig, panSNHaplotype, panSNSample } from "./util.js";
2
+ // A tab can occur in neither half: both come out of BED columns
3
+ function qualifiedKey(assemblyName, refName) {
4
+ return `${assemblyName}\t${refName}`;
5
+ }
6
+ /**
7
+ * A file's sequence names under each spelling a caller may ask by: the file's
8
+ * own (`GRCh38#0#chr6`), and a PanSN name's contig under its sample (`chr6`
9
+ * on `GRCh38`) and under its haplotype (`chr6` on `GRCh38#0`). A pggb name
10
+ * with no haplotype field, `chm13#chr1`, is `chr1` on `chm13`.
11
+ */
12
+ export function refNameLookup(names) {
13
+ const lookup = new Map();
14
+ for (const name of names) {
15
+ lookup.set(name, name);
16
+ const sample = panSNSample(name);
17
+ if (sample !== name) {
18
+ const contig = panSNContig(name);
19
+ lookup.set(qualifiedKey(sample, contig), name);
20
+ const haplotype = panSNHaplotype(name);
21
+ if (haplotype !== undefined) {
22
+ lookup.set(qualifiedKey(haplotype, contig), name);
23
+ }
24
+ }
25
+ }
26
+ return lookup;
27
+ }
28
+ /**
29
+ * The file's name for `refName` on `assemblyName`, a sample or a haplotype,
30
+ * else for `refName` as the file spells it; undefined when it holds neither
31
+ */
32
+ export function resolveRefName(lookup, assemblyName, refName) {
33
+ var _a;
34
+ return (_a = lookup.get(qualifiedKey(assemblyName, refName))) !== null && _a !== void 0 ? _a : lookup.get(refName);
35
+ }
package/esm/util.d.ts CHANGED
@@ -34,6 +34,8 @@ export declare class RegionTooLargeError extends Error {
34
34
  readonly fitsBp: number;
35
35
  constructor(message: string, limit: number, windowBp: number, fitsBp: number);
36
36
  }
37
+ export declare function panSNSample(refName: string): string;
38
+ export declare function panSNContig(refName: string): string;
37
39
  export declare function panSNHaplotype(refName: string): string | undefined;
38
40
  export declare function panSNMatchesPrefix(refName: string, prefix: string | undefined): boolean;
39
41
  export declare function bpString(bp: number): string;
package/esm/util.js CHANGED
@@ -14,6 +14,19 @@ export class RegionTooLargeError extends Error {
14
14
  }
15
15
  }
16
16
  const SEP = '#';
17
+ // `grape#1#chr1` -> `grape`
18
+ export function panSNSample(refName) {
19
+ return refName.split(SEP)[0];
20
+ }
21
+ // `grape#1#chr1` and `grape#chr1` -> `chr1`
22
+ export function panSNContig(refName) {
23
+ const parts = refName.split(SEP);
24
+ return parts.length >= 3
25
+ ? parts.slice(2).join(SEP)
26
+ : parts.length === 2
27
+ ? parts[1]
28
+ : refName;
29
+ }
17
30
  export function panSNHaplotype(refName) {
18
31
  const parts = refName.split(SEP);
19
32
  return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : undefined;
@@ -24,7 +24,7 @@ export declare class RefPath {
24
24
  }
25
25
  export declare class WalkRowError extends Error {
26
26
  }
27
- export interface Format2Chunk {
27
+ export interface Chunk {
28
28
  path: RefPath;
29
29
  routes: Route[];
30
30
  walks: {
@@ -34,6 +34,7 @@ export interface Format2Chunk {
34
34
  lengths: Map<number, number>;
35
35
  }
36
36
  export interface DecodedChunk {
37
+ sequences: Map<number, string>;
37
38
  rows: WalkRow[];
38
39
  nodes: Map<number, PlacedNode>;
39
40
  ends: {
@@ -43,13 +44,13 @@ export interface DecodedChunk {
43
44
  first: number;
44
45
  last: number;
45
46
  }[];
46
- chunk: Format2Chunk;
47
+ chunk: Chunk;
47
48
  }
48
49
  /**
49
50
  * Decodes one chunk's rows, each split on tabs. `keep` names the walk rows
50
51
  * whose steps a cut wants, undefined for every one.
51
52
  */
52
- export declare function decodeChunk(rows: string[][], keep: ((name: string) => boolean) | undefined): DecodedChunk;
53
+ export declare function decodeChunk(rows: string[][], keep: ((name: string) => boolean) | undefined, withSequences?: boolean): DecodedChunk;
53
54
  /**
54
55
  * The positions of the cut's nodes off R, and the links among the cut's
55
56
  * nodes, from the chunks a cut read: `kept` holds the node ids the cut draws.
@@ -57,7 +58,7 @@ export declare function decodeChunk(rows: string[][], keep: ((name: string) => b
57
58
  * those visiting it in the chunks read, at its earliest offset, as format 1's
58
59
  * node rows had it. Links are R's and the routes', both ends in the cut.
59
60
  */
60
- export declare function placeAndLink(chunks: Format2Chunk[], kept: Set<number>, place: (id: number, node: PlacedNode) => void, link: (a: number, b: number) => void): void;
61
+ export declare function placeAndLink(chunks: Chunk[], kept: Set<number>, place: (id: number, node: PlacedNode) => void, link: (a: number, b: number) => void): void;
61
62
  export declare function referenceNodes(rows: string[][]): (PlacedNode & {
62
63
  id: number;
63
64
  })[];
@@ -1,5 +1,5 @@
1
- // Format 2 of gfa-to-tabix --walks, one file per reference (that repo's
2
- // docs/walks-format.md). A chunk lists its `ref` rows, which joined in
1
+ // A chunk of gfa-to-tabix's walk format 2, one file per reference (that
2
+ // repo's docs/walks-format.md). A chunk lists its `ref` rows, which joined in
3
3
  // hapOffset order are the chunk's reference path R; a `nodes` row with the
4
4
  // lengths of the nodes its routes visit; `route` rows, each a way off R; and a
5
5
  // `walk` row per piece of every other path, naming the routes it takes in
@@ -286,16 +286,31 @@ function decodeWalk(path, routes, cols, choices, n) {
286
286
  }
287
287
  return { ids, rev };
288
288
  }
289
+ // The bases an SQ:Z: tag at or after column `from` joins, one node's per
290
+ // length, as `ids` names them
291
+ function sliceBases(cols, from, ids, lengths, out) {
292
+ const tag = cols.slice(from).find(c => c.startsWith('SQ:Z:'));
293
+ if (tag === undefined) {
294
+ return;
295
+ }
296
+ let at = 5;
297
+ for (let k = 0; k < ids.length; k++) {
298
+ const length = lengths[k];
299
+ out.set(ids[k], tag.slice(at, at + length));
300
+ at += length;
301
+ }
302
+ }
289
303
  /**
290
304
  * Decodes one chunk's rows, each split on tabs. `keep` names the walk rows
291
305
  * whose steps a cut wants, undefined for every one.
292
306
  */
293
- export function decodeChunk(rows, keep) {
307
+ export function decodeChunk(rows, keep, withSequences = false) {
294
308
  var _a, _b, _c;
295
309
  const path = new RefPath();
296
310
  const lengths = new Map();
297
311
  const routes = [];
298
312
  const out = {
313
+ sequences: new Map(),
299
314
  rows: [],
300
315
  nodes: new Map(),
301
316
  ends: [],
@@ -309,6 +324,9 @@ export function decodeChunk(rows, keep) {
309
324
  const nodeLengths = numbers(cols[10]);
310
325
  const hapOffset = +cols[6];
311
326
  path.add(handles, nodeLengths);
327
+ if (withSequences) {
328
+ sliceBases(cols, 11, handles.map(idOf), nodeLengths, out.sequences);
329
+ }
312
330
  let start = hapOffset;
313
331
  for (let k = 0; k < handles.length; k++) {
314
332
  const id = idOf(handles[k]);
@@ -339,7 +357,6 @@ export function decodeChunk(rows, keep) {
339
357
  hapOffset,
340
358
  piece: +cols[7],
341
359
  n: handles.length,
342
- enc: '',
343
360
  steps: {
344
361
  ids: Int32Array.from(handles, idOf),
345
362
  rev: Uint8Array.from(handles, h => h & 1),
@@ -355,8 +372,12 @@ export function decodeChunk(rows, keep) {
355
372
  let id = 0;
356
373
  for (let k = 0; k < ids.length; k++) {
357
374
  id += ids[k];
375
+ ids[k] = id;
358
376
  lengths.set(id, nodeLengths[k]);
359
377
  }
378
+ if (withSequences) {
379
+ sliceBases(cols, 6, ids, nodeLengths, out.sequences);
380
+ }
360
381
  }
361
382
  else if (cols[3] === 'route') {
362
383
  const entry = tokens(cols[4], previous, []);
@@ -423,7 +444,6 @@ export function decodeChunk(rows, keep) {
423
444
  hapOffset: +cols[6],
424
445
  piece: +cols[7],
425
446
  n,
426
- enc: '',
427
447
  steps: decodeWalk(path, routes, cols, choices, n),
428
448
  ...tags(cols, 12, cols[0]),
429
449
  });
@@ -1,5 +1,9 @@
1
1
  import { WalkGraph } from './walkRows.ts';
2
2
  import type { BudgetNames, WalkHeader } from './walkRows.ts';
3
+ export declare const WALK_BUDGETS: {
4
+ stepBudget: number;
5
+ byteBudget: number;
6
+ };
3
7
  /**
4
8
  * A Tabix-indexed file as the reader uses one: @gmod/tabix's
5
9
  * TabixIndexedFile is one. `bytesForRegions` estimates the compressed bytes a
@@ -24,11 +28,10 @@ export interface TabixSource {
24
28
  signal?: AbortSignal;
25
29
  }): Promise<number>;
26
30
  }
27
- export interface WalkFileSet {
28
- walks: TabixSource;
29
- nodes?: TabixSource;
30
- links?: TabixSource;
31
+ export declare class WalkFormatError extends Error {
32
+ name: string;
31
33
  }
34
+ export declare function checkRange(refName: string, start: number, end: number): void;
32
35
  export interface WalkCutOptions {
33
36
  signal?: AbortSignal;
34
37
  keep?: (name: string) => boolean;
@@ -44,24 +47,33 @@ export type WalkReaderHeader = WalkHeader & {
44
47
  lookback: number;
45
48
  };
46
49
  /**
47
- * A walk-indexed graph's three files, read a window at a time: one read of
48
- * each over whole chunks, from the lookback chunks before the window to its
49
- * end. The indexes first estimate the bytes those reads fetch, and a window
50
- * past the byte budget reads no row. Only the walks `keep` accepts are
51
- * decoded. The step budget bounds the steps the cut keeps, and rows holding
52
- * READ_STEPS_PER_KEPT times as many stop the reads, which go out together.
50
+ * A `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz`, read a
51
+ * window at a time: one read over whole chunks, from the lookback chunks
52
+ * before the window to its end, each chunk decoded once. The index first
53
+ * estimates the bytes the read fetches, and a window past the byte budget
54
+ * reads no row. Only the walks `keep` accepts are decoded. The step budget
55
+ * bounds the steps the cut keeps, and rows holding READ_STEPS_PER_KEPT times
56
+ * as many stop the read.
53
57
  */
54
58
  export declare class WalkReader {
55
59
  private headerRead;
56
- private readonly files;
57
- private readonly chunkWithoutHeader;
58
- constructor(files: WalkFileSet, chunkWithoutHeader?: number);
60
+ private lookupRead;
61
+ private readonly walks;
62
+ constructor(walks: TabixSource);
59
63
  header(opts?: {
60
64
  signal?: AbortSignal;
61
65
  }): Promise<WalkReaderHeader>;
62
66
  referenceSequences(opts?: {
63
67
  signal?: AbortSignal;
64
68
  }): Promise<string[]>;
69
+ /**
70
+ * The walk file's sequence for `refName` on `sample`, or for `refName` as
71
+ * the file spells it: `chr6` on `GRCh38` is `GRCh38#0#chr6`. Undefined when
72
+ * the file holds neither.
73
+ */
74
+ sequenceFor(sample: string, refName: string, opts?: {
75
+ signal?: AbortSignal;
76
+ }): Promise<string | undefined>;
65
77
  queryStart(start: number, opts?: {
66
78
  signal?: AbortSignal;
67
79
  }): Promise<number>;
package/esm/walkReader.js CHANGED
@@ -1,5 +1,6 @@
1
- import { decodeChunk, pieceLinks } from "./walkFormat2.js";
2
- import { WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, parseWalkRow, stepBudgetError, walkCut, walkHeader, walkRowName, } from "./walkRows.js";
1
+ import { refNameLookup, resolveRefName } from "./refNames.js";
2
+ import { decodeChunk, pieceLinks } from "./walkChunk.js";
3
+ import { WalkGraph, byteBudgetError, chunkQueryStart, joinPieces, keptStepsError, lookbackChunks, stepBudgetError, walkCut, walkHeader, } from "./walkRows.js";
3
4
  // A window reads whole chunks and keeps the steps on it, so a narrow window
4
5
  // reads several times what it keeps: 464 haplotypes across the 1.5 kb ABCA7
5
6
  // repeat read 5.0 M steps and keep 1.16 M, and the cut takes 0.4 s from warm
@@ -12,17 +13,22 @@ const FOLLOW_ROUNDS = 3;
12
13
  const FOLLOW_CHUNKS = 4;
13
14
  // bp of reference a cut keeps past each end of the window
14
15
  const WALK_CONTEXT = 1000;
16
+ // The budgets the JBrowse graph plugin and BandageJS cut a browser's window by
17
+ export const WALK_BUDGETS = { stepBudget: 4000000, byteBudget: 8000000 };
18
+ // A walk file gfa-to-tabix 0.12 or earlier wrote, which the reader refuses
19
+ export class WalkFormatError extends Error {
20
+ constructor() {
21
+ super(...arguments);
22
+ this.name = 'WalkFormatError';
23
+ }
24
+ }
15
25
  // @gmod/tabix loops forever on a NaN end and reads nothing for a NaN start
16
26
  // (GMOD/tabix-js#157), so every query refuses a range that is not finite first
17
- function checkRange(refName, start, end) {
27
+ export function checkRange(refName, start, end) {
18
28
  if (!Number.isFinite(start) || !Number.isFinite(end)) {
19
29
  throw new Error(`${refName}:${start}-${end} is not a finite range to query the graph's index for`);
20
30
  }
21
31
  }
22
- function getLines(file, refName, start, end, opts) {
23
- checkRange(refName, start, end);
24
- return file.getLines(refName, start, end, opts);
25
- }
26
32
  // Settles like `promise` unless `signal` aborts first
27
33
  function raceAbort(promise, signal) {
28
34
  if (!signal) {
@@ -42,26 +48,29 @@ function raceAbort(promise, signal) {
42
48
  });
43
49
  }
44
50
  /**
45
- * A walk-indexed graph's three files, read a window at a time: one read of
46
- * each over whole chunks, from the lookback chunks before the window to its
47
- * end. The indexes first estimate the bytes those reads fetch, and a window
48
- * past the byte budget reads no row. Only the walks `keep` accepts are
49
- * decoded. The step budget bounds the steps the cut keeps, and rows holding
50
- * READ_STEPS_PER_KEPT times as many stop the reads, which go out together.
51
+ * A `gfa-to-tabix --walks` file, `<prefix>.<sample>.walks.bed.gz`, read a
52
+ * window at a time: one read over whole chunks, from the lookback chunks
53
+ * before the window to its end, each chunk decoded once. The index first
54
+ * estimates the bytes the read fetches, and a window past the byte budget
55
+ * reads no row. Only the walks `keep` accepts are decoded. The step budget
56
+ * bounds the steps the cut keeps, and rows holding READ_STEPS_PER_KEPT times
57
+ * as many stop the read.
51
58
  */
52
59
  export class WalkReader {
53
- constructor(files, chunkWithoutHeader = 65536) {
54
- this.files = files;
55
- this.chunkWithoutHeader = chunkWithoutHeader;
60
+ constructor(walks) {
61
+ this.walks = walks;
56
62
  }
57
63
  // The header, read once for every caller: one caller giving up stops only
58
64
  // its own wait, and a read that fails is tried again by the next caller
59
65
  header(opts = {}) {
60
66
  if (!this.headerRead) {
61
- const read = this.files.walks.getHeaderLines().then(lines => {
67
+ const read = this.walks.getHeaderLines().then(lines => {
62
68
  var _a;
63
69
  const header = walkHeader(lines);
64
- const chunk = (_a = header.chunk) !== null && _a !== void 0 ? _a : this.chunkWithoutHeader;
70
+ if (header.format !== 2) {
71
+ throw new WalkFormatError('This walk file is format 1, from gfa-to-tabix 0.12 or earlier; rebuild it with gfa-to-tabix 1.0 or later');
72
+ }
73
+ const chunk = (_a = header.chunk) !== null && _a !== void 0 ? _a : 65536;
65
74
  return {
66
75
  ...header,
67
76
  chunk,
@@ -79,36 +88,40 @@ export class WalkReader {
79
88
  }
80
89
  // The reference sequences the walk file holds windows on
81
90
  referenceSequences(opts = {}) {
82
- return this.files.walks.getReferenceSequenceNames(opts);
91
+ return this.walks.getReferenceSequenceNames(opts);
92
+ }
93
+ /**
94
+ * The walk file's sequence for `refName` on `sample`, or for `refName` as
95
+ * the file spells it: `chr6` on `GRCh38` is `GRCh38#0#chr6`. Undefined when
96
+ * the file holds neither.
97
+ */
98
+ async sequenceFor(sample, refName, opts = {}) {
99
+ if (!this.lookupRead) {
100
+ const read = this.walks.getReferenceSequenceNames().then(refNameLookup);
101
+ read.catch(() => {
102
+ if (this.lookupRead === read) {
103
+ this.lookupRead = undefined;
104
+ }
105
+ });
106
+ this.lookupRead = read;
107
+ }
108
+ const lookup = await raceAbort(this.lookupRead, opts.signal);
109
+ return resolveRefName(lookup, sample, refName);
83
110
  }
84
111
  async queryStart(start, opts = {}) {
85
112
  const { chunk, lookback } = await this.header(opts);
86
113
  return chunkQueryStart(start, chunk, lookback);
87
114
  }
88
115
  async cut(refName, region, opts = {}) {
89
- const { signal, keep, context = WALK_CONTEXT, stepBudget = Infinity, byteBudget = Infinity, } = opts;
90
- const { chunk, lookback, format } = await this.header({ signal });
91
- // format 2 holds the whole cut in its walk file
92
- const format2 = format === 2;
93
- const nodeFile = this.files.nodes;
94
- const linkFile = opts.links === false ? undefined : this.files.links;
95
- if (!format2 && !nodeFile) {
96
- throw new Error('A format 1 walk file needs its node file: open it with `nodes`');
97
- }
98
- const links = format2 ? opts.links !== false : linkFile !== undefined;
116
+ var _a;
117
+ const { signal, keep, context = WALK_CONTEXT, stepBudget = Infinity, byteBudget = Infinity, links = true, } = opts;
118
+ const { chunk, lookback } = await this.header({ signal });
99
119
  const from = chunkQueryStart(region.start, chunk, lookback);
100
- const files = format2
101
- ? [this.files.walks]
102
- : [this.files.walks, nodeFile, ...(linkFile ? [linkFile] : [])];
103
- const budgeted = Number.isFinite(byteBudget) &&
104
- files.every(file => file.bytesForRegions !== undefined);
105
- const bytesFor = async (spans) => {
106
- if (!budgeted) {
107
- return 0;
108
- }
109
- const sizes = await Promise.all(files.map(file => file.bytesForRegions(spans.map(span => ({ refName, ...span })), { signal })));
110
- return sizes.reduce((sum, bytes) => sum + bytes, 0);
111
- };
120
+ const bytesForRegions = (_a = this.walks.bytesForRegions) === null || _a === void 0 ? void 0 : _a.bind(this.walks);
121
+ const budgeted = Number.isFinite(byteBudget) && bytesForRegions !== undefined;
122
+ const bytesFor = async (spans) => budgeted
123
+ ? bytesForRegions(spans.map(span => ({ refName, ...span })), { signal })
124
+ : 0;
112
125
  const tooLarge = budgeted
113
126
  ? await byteBudgetError(async (end) => {
114
127
  checkRange(refName, from, end);
@@ -133,49 +146,39 @@ export class WalkReader {
133
146
  context,
134
147
  sequences: opts.sequences,
135
148
  });
136
- const read = (file, lineCallback, start = from, end = region.end) => getLines(file, refName, start, end, {
137
- signal: reads.signal,
138
- lineCallback,
139
- });
140
- const addRow = (line) => {
141
- if (keep === undefined || keep(walkRowName(line))) {
142
- rows.push(parseWalkRow(line));
143
- }
144
- };
145
- const addNode = (line) => {
146
- graph.addNode(line);
147
- };
148
- const addLink = (line) => {
149
- graph.addLink(line);
150
- };
151
149
  const decoded = new Set();
152
150
  const ends = [];
153
- // Format 2: each chunk read is decoded once, into the rows, nodes and
154
- // links format 1 would have filed, and the links between pieces read
155
- const readChunks = async (start = from, end = region.end) => {
151
+ // each chunk read is decoded once, into its rows, nodes and the links
152
+ // between pieces
153
+ const readChunks = async (start, end) => {
154
+ checkRange(refName, start, end);
156
155
  const byChunk = new Map();
157
- await read(this.files.walks, line => {
158
- const cols = line.split('\t');
159
- const cs = +cols[1];
160
- const held = byChunk.get(cs);
161
- if (held) {
162
- held.push(cols);
163
- }
164
- else {
165
- byChunk.set(cs, [cols]);
166
- }
167
- }, start, end);
156
+ await this.walks.getLines(refName, start, end, {
157
+ signal: reads.signal,
158
+ lineCallback: line => {
159
+ const cols = line.split('\t');
160
+ const cs = +cols[1];
161
+ const held = byChunk.get(cs);
162
+ if (held) {
163
+ held.push(cols);
164
+ }
165
+ else {
166
+ byChunk.set(cs, [cols]);
167
+ }
168
+ },
169
+ });
168
170
  for (const [cs, chunkRows] of byChunk) {
169
171
  if (decoded.has(cs)) {
170
172
  continue;
171
173
  }
172
174
  decoded.add(cs);
173
- const chunkCut = decodeChunk(chunkRows, keep);
175
+ const chunkCut = decodeChunk(chunkRows, keep, opts.sequences);
174
176
  rows.push(...chunkCut.rows);
175
177
  for (const [id, node] of chunkCut.nodes) {
176
178
  graph.placeNode(id, node);
177
179
  }
178
- graph.addFormat2Chunk(chunkCut.chunk);
180
+ graph.addSequences(chunkCut.sequences);
181
+ graph.addChunk(chunkCut.chunk);
179
182
  ends.push(...chunkCut.ends);
180
183
  }
181
184
  if (links) {
@@ -184,20 +187,12 @@ export class WalkReader {
184
187
  }
185
188
  }
186
189
  };
187
- const checkSteps = () => {
188
- const error = stepBudgetError(rows, stepBudget, READ_STEPS_PER_KEPT, region, chunk, keep !== undefined, opts.budgetNames);
189
- if (error) {
190
- throw error;
191
- }
192
- };
193
190
  try {
194
- await (format2
195
- ? readChunks().then(checkSteps)
196
- : Promise.all([
197
- read(this.files.walks, addRow).then(checkSteps),
198
- read(nodeFile, addNode),
199
- ...(linkFile ? [read(linkFile, addLink)] : []),
200
- ]));
191
+ await readChunks(from, region.end);
192
+ const tooManyRead = stepBudgetError(rows, stepBudget, READ_STEPS_PER_KEPT, region, chunk, keep !== undefined, opts.budgetNames);
193
+ if (tooManyRead) {
194
+ throw tooManyRead;
195
+ }
201
196
  // the chunks read, first to last, which a follow widens to the
202
197
  // farthest chunk a cut-off walk goes on in, so the reference between
203
198
  // is read too
@@ -224,13 +219,7 @@ export class WalkReader {
224
219
  }
225
220
  first = Math.min(first, ...starts);
226
221
  last = Math.max(last, ...starts);
227
- await Promise.all(spans.flatMap(({ start, end }) => format2
228
- ? [readChunks(start, end)]
229
- : [
230
- read(this.files.walks, addRow, start, end),
231
- read(nodeFile, addNode, start, end),
232
- ...(linkFile ? [read(linkFile, addLink, start, end)] : []),
233
- ]));
222
+ await Promise.all(spans.map(({ start, end }) => readChunks(start, end)));
234
223
  cut = walkCut(joinPieces(rows), graph.nodes, refName);
235
224
  }
236
225
  }
package/esm/walkRows.d.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  import { RegionTooLargeError } from './util.ts';
2
2
  import type { GraphTables } from './util.ts';
3
- import type { Format2Chunk } from './walkFormat2.ts';
3
+ import type { Chunk } from './walkChunk.ts';
4
4
  export interface WalkRow {
5
5
  name: string;
6
6
  fragStart: number;
7
7
  hapOffset: number;
8
8
  piece: number;
9
9
  n: number;
10
- enc: string;
11
- steps?: {
10
+ steps: {
12
11
  ids: Int32Array;
13
12
  rev: Uint8Array;
14
13
  };
@@ -34,8 +33,6 @@ export interface WalkWindow {
34
33
  context: number;
35
34
  sequences?: boolean;
36
35
  }
37
- export declare function walkRowName(line: string): string;
38
- export declare function parseWalkRow(line: string): WalkRow;
39
36
  export interface WalkHeader {
40
37
  format?: number;
41
38
  chunk?: number;
@@ -44,11 +41,10 @@ export interface WalkHeader {
44
41
  haplotypes: string[];
45
42
  }
46
43
  /**
47
- * What a walk file's header states: its chunk size in a
48
- * `#walks\tchunk:i:65536` line, from gfa-to-tabix 0.4.0 a
49
- * `#reference\tGRCh38` line per reference sample and a
50
- * `#haplotype\tHG002#1` line per other haplotype with rows, and from 0.5.0
51
- * the longest node in bp as `maxnode:i:1024` beside the chunk size
44
+ * What a walk file's header states: a `#walks` line with the format, the
45
+ * chunk size and the longest node in bp, `format:i:2\tchunk:i:65536\tmaxnode:i:1024`,
46
+ * a `#reference\tGRCh38` line naming the reference sample and a
47
+ * `#haplotype\tHG002#1` line per other haplotype with rows
52
48
  */
53
49
  export declare function walkHeader(lines: string[]): WalkHeader;
54
50
  /**
@@ -62,10 +58,12 @@ export declare function walkHeader(lines: string[]): WalkHeader;
62
58
  export declare function lookbackChunks(maxNode: number | undefined, chunk: number): number;
63
59
  export declare function chunkQueryStart(start: number, chunk: number, lookback?: number): number;
64
60
  export declare function walkNameFilter(wanted: string[] | undefined, referenceHaplotype: string | undefined): ((name: string) => boolean) | undefined;
65
- export declare function decodeSteps(enc: string, n: number, ids?: Int32Array<ArrayBuffer>, rev?: Uint8Array<ArrayBuffer>, at?: number): {
66
- ids: Int32Array<ArrayBuffer>;
67
- rev: Uint8Array<ArrayBuffer>;
68
- };
61
+ /**
62
+ * The walks a cut on `refName` decodes: its own and those `wanted` names. A
63
+ * pggb P-line names a reference with no haplotype field (`chm13#chr1`), so the
64
+ * filter matches the reference by its whole name.
65
+ */
66
+ export declare function walksKept(wanted: string[] | undefined, refName: string): ((name: string) => boolean) | undefined;
69
67
  export interface WalkRun {
70
68
  name: string;
71
69
  hapOffset: number;
@@ -103,24 +101,20 @@ export declare function walkCut(runs: WalkRun[], nodes: Map<number, WalkNode>, r
103
101
  open: WalkChunk[];
104
102
  };
105
103
  /**
106
- * The node and link rows a walk cut reads, parsed as they arrive, and the
107
- * tables of the cut they make with its walks. A node filed under two chunks
108
- * arrives twice and is read once; columns past a node row's eighth (LN:i:,
109
- * SQ:Z: and whatever follows) are read only for the bases a window asks for.
104
+ * The nodes and links of the chunks a cut reads, and the tables of the cut
105
+ * they make with its walks. A node in two chunks is placed once.
110
106
  */
111
107
  export declare class WalkGraph {
112
108
  readonly nodes: Map<number, WalkNode>;
113
109
  readonly sequences: Map<number, string>;
114
110
  private links;
115
- private readonly format2;
116
- private readonly t;
111
+ private readonly chunks;
117
112
  private readonly window;
118
113
  constructor(window: WalkWindow);
119
- addNode(line: string): void;
120
114
  placeNode(id: number, node: Omit<WalkNode, 'onWindow'>): void;
121
115
  addLinkEnds(source: number, target: number): void;
122
- addFormat2Chunk(chunk: Format2Chunk): void;
123
- addLink(line: string): void;
116
+ addChunk(chunk: Chunk): void;
117
+ addSequences(sequences: Map<number, string>): void;
124
118
  /**
125
119
  * The cut as typed arrays, which the worker transfers rather than copies:
126
120
  * nodes by reference position, each link once in the order its L line
package/esm/walkRows.js CHANGED
@@ -1,52 +1,14 @@
1
- import { RegionTooLargeError, bpString, panSNMatchesPrefix } from "./util.js";
2
- import { placeAndLink } from "./walkFormat2.js";
3
- // The name column, read without splitting the steps, so a row the cut does
4
- // not want costs a few indexOf calls
5
- export function walkRowName(line) {
6
- let at = -1;
7
- for (let k = 0; k < 3; k++) {
8
- at = line.indexOf('\t', at + 1);
9
- }
10
- return line.slice(at + 1, line.indexOf('\t', at + 1));
11
- }
12
- export function parseWalkRow(line) {
13
- const c = line.split('\t');
14
- const row = {
15
- chunkStart: +c[1],
16
- name: c[3],
17
- fragStart: +c[4],
18
- hapOffset: +c[5],
19
- piece: +c[6],
20
- n: +c[7],
21
- enc: c[8],
22
- };
23
- for (let k = 9; k < c.length; k++) {
24
- const tag = c[k];
25
- const side = tag.startsWith('pv:')
26
- ? 'prev'
27
- : tag.startsWith('nx:')
28
- ? 'next'
29
- : undefined;
30
- if (side) {
31
- const at = tag.lastIndexOf(':');
32
- row[side] = {
33
- refName: tag[3] === 'Z' ? tag.slice(5, at) : c[0],
34
- start: +tag.slice(at + 1),
35
- };
36
- }
37
- }
38
- return row;
39
- }
1
+ import { RegionTooLargeError, bpString, panSNHaplotype, panSNMatchesPrefix, } from "./util.js";
2
+ import { placeAndLink } from "./walkChunk.js";
40
3
  function integerTag(line, tag) {
41
4
  const match = new RegExp(`(?:^|\\t)${tag}:i:(\\d+)`).exec(line);
42
5
  return match ? +match[1] : undefined;
43
6
  }
44
7
  /**
45
- * What a walk file's header states: its chunk size in a
46
- * `#walks\tchunk:i:65536` line, from gfa-to-tabix 0.4.0 a
47
- * `#reference\tGRCh38` line per reference sample and a
48
- * `#haplotype\tHG002#1` line per other haplotype with rows, and from 0.5.0
49
- * the longest node in bp as `maxnode:i:1024` beside the chunk size
8
+ * What a walk file's header states: a `#walks` line with the format, the
9
+ * chunk size and the longest node in bp, `format:i:2\tchunk:i:65536\tmaxnode:i:1024`,
10
+ * a `#reference\tGRCh38` line naming the reference sample and a
11
+ * `#haplotype\tHG002#1` line per other haplotype with rows
50
12
  */
51
13
  export function walkHeader(lines) {
52
14
  var _a, _b, _c;
@@ -91,30 +53,14 @@ export function walkNameFilter(wanted, referenceHaplotype) {
91
53
  : (name) => panSNMatchesPrefix(name, referenceHaplotype) ||
92
54
  wanted.some(prefix => panSNMatchesPrefix(name, prefix));
93
55
  }
94
- export function decodeSteps(enc, n, ids = new Int32Array(n), rev = new Uint8Array(n), at = 0) {
95
- let prev = 0;
96
- let i = at;
97
- let v = 0;
98
- let neg = false;
99
- for (let p = 0; p <= enc.length; p++) {
100
- const ch = p < enc.length ? enc.charCodeAt(p) : 44;
101
- if (ch === 44) {
102
- const d = neg ? -v : v;
103
- const r = d & 1;
104
- rev[i] = r;
105
- prev += (d - r) / 2;
106
- ids[i++] = prev;
107
- v = 0;
108
- neg = false;
109
- }
110
- else if (ch === 45) {
111
- neg = true;
112
- }
113
- else {
114
- v = v * 10 + (ch - 48);
115
- }
116
- }
117
- return { ids, rev };
56
+ /**
57
+ * The walks a cut on `refName` decodes: its own and those `wanted` names. A
58
+ * pggb P-line names a reference with no haplotype field (`chm13#chr1`), so the
59
+ * filter matches the reference by its whole name.
60
+ */
61
+ export function walksKept(wanted, refName) {
62
+ var _a;
63
+ return walkNameFilter(wanted, (_a = panSNHaplotype(refName)) !== null && _a !== void 0 ? _a : refName);
118
64
  }
119
65
  export function joinPieces(rows) {
120
66
  const byPath = new Map();
@@ -148,13 +94,8 @@ function decodeRun(pieces) {
148
94
  const rev = new Uint8Array(n);
149
95
  let at = 0;
150
96
  for (const p of pieces) {
151
- if (p.steps) {
152
- ids.set(p.steps.ids, at);
153
- rev.set(p.steps.rev, at);
154
- }
155
- else {
156
- decodeSteps(p.enc, p.n, ids, rev, at);
157
- }
97
+ ids.set(p.steps.ids, at);
98
+ rev.set(p.steps.rev, at);
158
99
  at += p.n;
159
100
  }
160
101
  const first = pieces[0];
@@ -308,50 +249,19 @@ export function walkCut(runs, nodes, refName) {
308
249
  }
309
250
  return { kept, fragments, open: [...open.values()] };
310
251
  }
311
- // The positions of a line's first `n` tabs, -1 past the last
312
- function tabs(line, n, out) {
313
- let at = -1;
314
- for (let k = 0; k < n; k++) {
315
- at = at === -1 && k > 0 ? -1 : line.indexOf('\t', at + 1);
316
- out[k] = at;
317
- }
318
- return out;
319
- }
320
252
  /**
321
- * The node and link rows a walk cut reads, parsed as they arrive, and the
322
- * tables of the cut they make with its walks. A node filed under two chunks
323
- * arrives twice and is read once; columns past a node row's eighth (LN:i:,
324
- * SQ:Z: and whatever follows) are read only for the bases a window asks for.
253
+ * The nodes and links of the chunks a cut reads, and the tables of the cut
254
+ * they make with its walks. A node in two chunks is placed once.
325
255
  */
326
256
  export class WalkGraph {
327
257
  constructor(window) {
328
258
  this.nodes = new Map();
329
259
  this.sequences = new Map();
330
260
  this.links = [];
331
- // format 2's chunks, whose nodes off R and links tables() fills in
332
- this.format2 = [];
333
- this.t = [];
261
+ // the chunks read, whose nodes off R and links tables() fills in
262
+ this.chunks = [];
334
263
  this.window = window;
335
264
  }
336
- addNode(line) {
337
- const t = tabs(line, 8, this.t);
338
- const id = +line.slice(t[2] + 1, t[3]);
339
- if (!this.nodes.has(id)) {
340
- if (this.window.sequences) {
341
- const at = line.indexOf('\tSQ:Z:');
342
- if (at >= 0) {
343
- const end = line.indexOf('\t', at + 1);
344
- this.sequences.set(id, line.slice(at + 6, end < 0 ? undefined : end));
345
- }
346
- }
347
- this.placeNode(id, {
348
- rank: +line.slice(t[3] + 1, t[4]),
349
- refName: line.slice(t[4] + 1, t[5]),
350
- start: +line.slice(t[5] + 1, t[6]),
351
- end: +line.slice(t[6] + 1, t[7] === -1 ? undefined : t[7]),
352
- });
353
- }
354
- }
355
265
  // A node with its position, unless the cut already holds it
356
266
  placeNode(id, node) {
357
267
  if (this.nodes.has(id)) {
@@ -370,13 +280,16 @@ export class WalkGraph {
370
280
  addLinkEnds(source, target) {
371
281
  this.links.push(source, target);
372
282
  }
373
- addFormat2Chunk(chunk) {
374
- this.format2.push(chunk);
283
+ addChunk(chunk) {
284
+ this.chunks.push(chunk);
375
285
  }
376
- // An endpoint is held as id * 2 + 1 when reversed
377
- addLink(line) {
378
- const t = tabs(line, 5, this.t);
379
- this.links.push(endpoint(line, t[2] + 1, t[3]), endpoint(line, t[3] + 1, t[4] === -1 ? line.length : t[4]));
286
+ // Bases of nodes a chunk's rows give, for a window that asks for them
287
+ addSequences(sequences) {
288
+ if (this.window.sequences) {
289
+ for (const [id, bases] of sequences) {
290
+ this.sequences.set(id, bases);
291
+ }
292
+ }
380
293
  }
381
294
  /**
382
295
  * The cut as typed arrays, which the worker transfers rather than copies:
@@ -386,8 +299,8 @@ export class WalkGraph {
386
299
  * for the same rows.
387
300
  */
388
301
  tables(kept, fragments) {
389
- if (this.format2.length > 0) {
390
- placeAndLink(this.format2, kept, (id, node) => {
302
+ if (this.chunks.length > 0) {
303
+ placeAndLink(this.chunks, kept, (id, node) => {
391
304
  const held = this.nodes.get(id);
392
305
  if (held) {
393
306
  this.nodes.set(id, { ...held, ...node });
@@ -498,9 +411,6 @@ export class WalkGraph {
498
411
  (a.name < b.name ? -1 : a.name > b.name ? 1 : a.hapStart - b.hapStart));
499
412
  }
500
413
  }
501
- function endpoint(line, from, to) {
502
- return (+line.slice(from, to - 1) * 2 + (line.charCodeAt(to - 1) === 45 ? 1 : 0));
503
- }
504
414
  export const BUDGET_NAMES = {
505
415
  steps: 'the step budget',
506
416
  bytes: 'a byte budget of',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/graph-walks",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Read gfa-to-tabix walk files by range requests and cut a window of a pangenome graph: the reference, every haplotype's walk through it, and the nodes and links they visit",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -20,39 +20,25 @@
20
20
  "haplotype"
21
21
  ],
22
22
  "type": "module",
23
- "packageManager": "pnpm@11.20.0",
24
23
  "exports": {
25
- ".": "./src/index.ts"
24
+ ".": {
25
+ "types": "./esm/index.d.ts",
26
+ "default": "./esm/index.js"
27
+ }
26
28
  },
27
29
  "publishConfig": {
28
- "access": "public",
29
- "exports": {
30
- ".": {
31
- "types": "./esm/index.d.ts",
32
- "default": "./esm/index.js"
33
- }
34
- }
30
+ "access": "public"
35
31
  },
36
32
  "files": [
37
33
  "esm"
38
34
  ],
39
- "scripts": {
40
- "build": "rm -rf esm && tsc -p tsconfig.build.json",
41
- "typecheck": "tsc --noEmit",
42
- "lint": "eslint --report-unused-disable-directives --max-warnings 0",
43
- "format": "prettier --write .",
44
- "format:check": "prettier --check .",
45
- "test": "vitest run",
46
- "prepack": "pnpm build",
47
- "preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build",
48
- "postversion": "git push --follow-tags"
49
- },
50
35
  "engines": {
51
36
  "node": ">=20"
52
37
  },
53
38
  "devDependencies": {
54
39
  "@eslint/js": "^10.0.1",
55
40
  "@gmod/tabix": "^3.10.1",
41
+ "@types/node": "^26.6.5",
56
42
  "eslint": "^10.12.0",
57
43
  "eslint-plugin-import-x": "^4.17.1",
58
44
  "generic-filehandle2": "^2.5.0",
@@ -60,5 +46,15 @@
60
46
  "typescript": "^6.0.3",
61
47
  "typescript-eslint": "^8.71.1",
62
48
  "vitest": "^5.0.3"
49
+ },
50
+ "scripts": {
51
+ "build": "rm -rf esm && tsc -p tsconfig.build.json",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "eslint --report-unused-disable-directives --max-warnings 0",
54
+ "format": "prettier --write .",
55
+ "format:check": "prettier --check .",
56
+ "test": "vitest run",
57
+ "preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build",
58
+ "postversion": "git push --follow-tags"
63
59
  }
64
- }
60
+ }