@jbrowse/text-indexing-core 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/esm/index.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- export * from './util.ts';
2
- export * from './types/common.ts';
1
+ export { decodeURIComponentNoThrow, defaultAttributesToIndex, defaultFeatureTypesToExclude, defaultIndexingPolicy, indexableAdapters, isSupportedIndexingAdapter, trackIndexingPolicy, } from './util.ts';
2
+ export type { Gff3IndexerOptions, Gff3Adapter, Gff3TabixAdapter, GtfAdapter, IndexableFormat, IndexerOptions, IndexingPolicy, LocalPathLocation, Track, UriLocation, VcfAdapter, VcfTabixAdapter, } from './util.ts';
3
+ export { createReadlineInterface, getLocalOrRemoteStream, guessAdapterFromFileName, isURL, makeLocation, parseAttributes, } from './types/common.ts';
4
+ export { TRIX_DIR, sanitizeForFilename, trixFileNames, trixFilePaths, trixFileUris, } from './trixPaths.ts';
3
5
  export { indexGff3 } from './types/gff3Adapter.ts';
6
+ export { indexGtf } from './types/gtfAdapter.ts';
4
7
  export { indexVcf } from './types/vcfAdapter.ts';
8
+ export type { TrackIndexProgress } from './indexFiles.ts';
9
+ export { writeTrixIndex } from './writeTrixIndex.ts';
package/esm/index.js CHANGED
@@ -1,4 +1,7 @@
1
- export * from "./util.js";
2
- export * from "./types/common.js";
3
- export { indexGff3 } from "./types/gff3Adapter.js";
4
- export { indexVcf } from "./types/vcfAdapter.js";
1
+ export { decodeURIComponentNoThrow, defaultAttributesToIndex, defaultFeatureTypesToExclude, defaultIndexingPolicy, indexableAdapters, isSupportedIndexingAdapter, trackIndexingPolicy, } from './util.js';
2
+ export { createReadlineInterface, getLocalOrRemoteStream, guessAdapterFromFileName, isURL, makeLocation, parseAttributes, } from './types/common.js';
3
+ export { TRIX_DIR, sanitizeForFilename, trixFileNames, trixFilePaths, trixFileUris, } from './trixPaths.js';
4
+ export { indexGff3 } from './types/gff3Adapter.js';
5
+ export { indexGtf } from './types/gtfAdapter.js';
6
+ export { indexVcf } from './types/vcfAdapter.js';
7
+ export { writeTrixIndex } from './writeTrixIndex.js';
@@ -0,0 +1,13 @@
1
+ import type { IndexingPolicy, Track } from './util.ts';
2
+ export interface TrackIndexProgress {
3
+ onStart?: (totalBytes: number) => void;
4
+ onUpdate?: (processedBytes: number) => void;
5
+ onDone?: () => void;
6
+ }
7
+ export declare function indexFiles({ tracks, policy, outDir, makeProgress, checkAbort, }: {
8
+ tracks: Track[];
9
+ policy: IndexingPolicy;
10
+ outDir: string;
11
+ makeProgress?: (trackId: string) => TrackIndexProgress;
12
+ checkAbort?: () => void;
13
+ }): AsyncGenerator<string, void, unknown>;
@@ -0,0 +1,51 @@
1
+ import { indexGff3 } from './types/gff3Adapter.js';
2
+ import { indexGtf } from './types/gtfAdapter.js';
3
+ import { indexVcf } from './types/vcfAdapter.js';
4
+ import { indexableAdapters, trackIndexingPolicy } from './util.js';
5
+ function getIndexingLocation(track, locationKey) {
6
+ const { adapter } = track;
7
+ // adapter[locationKey] is the canonical location object, e.g. gffGzLocation;
8
+ // fall back to the adapter itself to support the shorthand where a bare `uri`
9
+ // sits directly on the adapter
10
+ const loc = (adapter?.[locationKey] ?? adapter);
11
+ return loc?.locationType === 'LocalPathLocation' ? loc.localPath : loc?.uri;
12
+ }
13
+ const noop = () => { };
14
+ // Streams index records for a set of tracks, each read under its own policy
15
+ export async function* indexFiles({ tracks, policy, outDir, makeProgress, checkAbort, }) {
16
+ for (const track of tracks) {
17
+ checkAbort?.();
18
+ const { adapter, trackId } = track;
19
+ const indexable = indexableAdapters[adapter?.type ?? ''];
20
+ const inLocation = indexable
21
+ ? getIndexingLocation(track, indexable.locationKey)
22
+ : undefined;
23
+ if (indexable && inLocation) {
24
+ const progress = makeProgress?.(trackId);
25
+ const { attributes, exclude, include } = trackIndexingPolicy(track, policy);
26
+ const common = {
27
+ config: track,
28
+ attributesToIndex: attributes,
29
+ inLocation,
30
+ outDir,
31
+ onStart: progress?.onStart ?? noop,
32
+ onUpdate: progress?.onUpdate ?? noop,
33
+ checkAbort,
34
+ };
35
+ if (indexable.format === 'gff3') {
36
+ yield* indexGff3({
37
+ ...common,
38
+ featureTypesToExclude: exclude,
39
+ featureTypesToInclude: include,
40
+ });
41
+ }
42
+ else if (indexable.format === 'gtf') {
43
+ yield* indexGtf(common);
44
+ }
45
+ else {
46
+ yield* indexVcf(common);
47
+ }
48
+ progress?.onDone?.();
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,31 @@
1
+ /** Every trix artifact lands in this directory under the output directory. */
2
+ export declare const TRIX_DIR = "trix";
3
+ export declare function sanitizeForFilename(name: string): string;
4
+ /**
5
+ * The three files an index called `name` writes, sanitized once.
6
+ *
7
+ * Everything that writes a trix artifact or points a config at one derives its
8
+ * paths from here, so a trackId holding a `/` or another Windows-invalid
9
+ * character names the same file for the writer as for the search reading it.
10
+ */
11
+ export declare function trixFileNames(name: string): {
12
+ ix: string;
13
+ ixx: string;
14
+ meta: string;
15
+ };
16
+ /** {@link trixFileNames} as filesystem paths under `outDir`, for a writer. */
17
+ export declare function trixFilePaths(outDir: string, name: string): {
18
+ ix: string;
19
+ ixx: string;
20
+ meta: string;
21
+ };
22
+ /**
23
+ * {@link trixFileNames} as URIs relative to the config, for an adapter conf.
24
+ * Always `/`-joined — a URI is not a filesystem path, so this one does not go
25
+ * through `path.join`, which would emit `\` on Windows.
26
+ */
27
+ export declare function trixFileUris(name: string): {
28
+ ix: string;
29
+ ixx: string;
30
+ meta: string;
31
+ };
@@ -0,0 +1,52 @@
1
+ import path from 'node:path';
2
+ /** Every trix artifact lands in this directory under the output directory. */
3
+ export const TRIX_DIR = 'trix';
4
+ // Windows reserves these device names even with an extension, so a track or
5
+ // assembly literally named e.g. NUL would otherwise yield an unusable NUL.ix
6
+ const windowsReservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
7
+ // makes `name` safe as a filename on Windows: replaces the invalid characters
8
+ // \ / : * ? " < > |, drops trailing dots/spaces (Windows silently strips them),
9
+ // and escapes reserved device names
10
+ export function sanitizeForFilename(name) {
11
+ const cleaned = name
12
+ .replaceAll(/[\\/:*?"<>|]/g, '_')
13
+ .replace(/(?<![. ])[. ]+$/, '');
14
+ return windowsReservedName.test(cleaned) ? `_${cleaned}` : cleaned;
15
+ }
16
+ /**
17
+ * The three files an index called `name` writes, sanitized once.
18
+ *
19
+ * Everything that writes a trix artifact or points a config at one derives its
20
+ * paths from here, so a trackId holding a `/` or another Windows-invalid
21
+ * character names the same file for the writer as for the search reading it.
22
+ */
23
+ export function trixFileNames(name) {
24
+ const safeName = sanitizeForFilename(name);
25
+ return {
26
+ ix: `${safeName}.ix`,
27
+ ixx: `${safeName}.ixx`,
28
+ meta: `${safeName}_meta.json`,
29
+ };
30
+ }
31
+ /** {@link trixFileNames} as filesystem paths under `outDir`, for a writer. */
32
+ export function trixFilePaths(outDir, name) {
33
+ const names = trixFileNames(name);
34
+ return {
35
+ ix: path.join(outDir, TRIX_DIR, names.ix),
36
+ ixx: path.join(outDir, TRIX_DIR, names.ixx),
37
+ meta: path.join(outDir, TRIX_DIR, names.meta),
38
+ };
39
+ }
40
+ /**
41
+ * {@link trixFileNames} as URIs relative to the config, for an adapter conf.
42
+ * Always `/`-joined — a URI is not a filesystem path, so this one does not go
43
+ * through `path.join`, which would emit `\` on Windows.
44
+ */
45
+ export function trixFileUris(name) {
46
+ const names = trixFileNames(name);
47
+ return {
48
+ ix: `${TRIX_DIR}/${names.ix}`,
49
+ ixx: `${TRIX_DIR}/${names.ixx}`,
50
+ meta: `${TRIX_DIR}/${names.meta}`,
51
+ };
52
+ }
@@ -1,7 +1,7 @@
1
- import readline from 'readline';
2
- import { Readable } from 'stream';
1
+ import readline from 'node:readline';
2
+ import { Readable } from 'node:stream';
3
3
  import type { LocalPathLocation, Track, UriLocation } from '../util.ts';
4
- export declare function isURL(FileName: string): boolean;
4
+ export declare function isURL(fileName: string): boolean;
5
5
  export declare function getLocalOrRemoteStream({ file, out, onStart, onUpdate, }: {
6
6
  file: string;
7
7
  out: string;
@@ -12,12 +12,3 @@ export declare function createReadlineInterface(stream: Readable, inLocation: st
12
12
  export declare function parseAttributes(infoString: string, decodeFunc: (s: string) => string): Record<string, string | undefined>;
13
13
  export declare function makeLocation(location: string, protocol: string): UriLocation | LocalPathLocation;
14
14
  export declare function guessAdapterFromFileName(filePath: string): Track;
15
- export declare function sanitizeForFilename(name: string): string;
16
- export declare function generateMeta({ configs, attributesToIndex, outDir, name, featureTypesToExclude, assemblyNames, }: {
17
- configs: Track[];
18
- attributesToIndex: string[];
19
- outDir: string;
20
- name: string;
21
- featureTypesToExclude: string[];
22
- assemblyNames: string[];
23
- }): void;
@@ -1,18 +1,19 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import readline from 'readline';
4
- import { Readable } from 'stream';
5
- import { fileURLToPath } from 'url';
6
- import { createGunzip } from 'zlib';
7
- export function isURL(FileName) {
8
- let url;
1
+ import { Buffer } from 'node:buffer';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline';
5
+ import { Readable } from 'node:stream';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createGunzip } from 'node:zlib';
8
+ import { indexableAdapters } from '../util.js';
9
+ export function isURL(fileName) {
9
10
  try {
10
- url = new URL(FileName);
11
+ const url = new URL(fileName);
12
+ return url.protocol === 'http:' || url.protocol === 'https:';
11
13
  }
12
- catch (_) {
14
+ catch {
13
15
  return false;
14
16
  }
15
- return url.protocol === 'http:' || url.protocol === 'https:';
16
17
  }
17
18
  function convertFileUrlToPath(fileUrl) {
18
19
  try {
@@ -22,9 +23,49 @@ function convertFileUrlToPath(fileUrl) {
22
23
  }
23
24
  }
24
25
  catch {
26
+ // not a valid URL
25
27
  }
26
28
  return undefined;
27
29
  }
30
+ // Turn a WHATWG ReadableStream into a node Readable by driving its reader.
31
+ //
32
+ // DO NOT replace this with `Readable.fromWeb(body)`. It looks equivalent and
33
+ // passes tests (undici's fetch body IS a node:stream/web stream), but the
34
+ // desktop indexing worker runs on Chromium's global fetch, whose body is a DOM
35
+ // ReadableStream from a different realm. `Readable.fromWeb`'s internal
36
+ // `instanceof` check rejects it with the baffling "must be an instance of
37
+ // ReadableStream. Received an instance of ReadableStream", and remote (URL)
38
+ // track indexing breaks. `getReader()` is standard on every WHATWG stream
39
+ // regardless of realm, so this works everywhere. See common.test.ts
40
+ // "foreign realm" regression test.
41
+ function webStreamToNodeReadable(reader) {
42
+ return new Readable({
43
+ read() {
44
+ reader
45
+ .read()
46
+ .then(({ done, value }) => {
47
+ this.push(done ? null : Buffer.from(value));
48
+ })
49
+ .catch((e) => {
50
+ this.destroy(e);
51
+ });
52
+ },
53
+ // Cancel the source when the Readable is destroyed early (e.g. an aborted
54
+ // index) so the underlying fetch connection is released rather than left
55
+ // hanging — the cleanup Readable.fromWeb would do for us if it accepted
56
+ // this stream.
57
+ destroy(error, callback) {
58
+ reader
59
+ .cancel(error ?? undefined)
60
+ .then(() => {
61
+ callback(error);
62
+ })
63
+ .catch(() => {
64
+ callback(error);
65
+ });
66
+ },
67
+ });
68
+ }
28
69
  export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, }) {
29
70
  let receivedBytes = 0;
30
71
  if (isURL(file)) {
@@ -38,19 +79,12 @@ export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, })
38
79
  if (!body) {
39
80
  throw new Error(`Failed to fetch ${file}: no response body`);
40
81
  }
82
+ // node-fetch gives a node Readable directly; global fetch gives a web
83
+ // ReadableStream we adapt (see webStreamToNodeReadable — do not swap in
84
+ // Readable.fromWeb here).
41
85
  const nodeStream = body instanceof Readable
42
86
  ? body
43
- : new Readable({
44
- async read() {
45
- const reader = body.getReader();
46
- let result = await reader.read();
47
- while (!result.done) {
48
- this.push(result.value);
49
- result = await reader.read();
50
- }
51
- this.push(null);
52
- },
53
- });
87
+ : webStreamToNodeReadable(body.getReader());
54
88
  nodeStream.on('data', chunk => {
55
89
  receivedBytes += chunk.length;
56
90
  onUpdate(receivedBytes);
@@ -58,6 +92,7 @@ export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, })
58
92
  return nodeStream;
59
93
  }
60
94
  else {
95
+ // Handle file:// URLs by converting to local path
61
96
  const localPath = convertFileUrlToPath(file) ?? file;
62
97
  const filename = path.isAbsolute(localPath)
63
98
  ? localPath
@@ -73,19 +108,10 @@ export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, })
73
108
  }
74
109
  }
75
110
  export function createReadlineInterface(stream, inLocation) {
76
- const isGzipped = /.b?gz$/.exec(inLocation);
77
- let inputStream;
78
- if (isGzipped) {
79
- const gunzip = createGunzip();
80
- inputStream = stream.pipe(gunzip);
81
- }
82
- else {
83
- inputStream = stream;
84
- }
85
- const rl = readline.createInterface({
86
- input: inputStream,
87
- });
88
- return rl;
111
+ const inputStream = /\.b?gz$/i.test(inLocation)
112
+ ? stream.pipe(createGunzip())
113
+ : stream;
114
+ return readline.createInterface({ input: inputStream });
89
115
  }
90
116
  export function parseAttributes(infoString, decodeFunc) {
91
117
  const result = {};
@@ -114,61 +140,33 @@ export function makeLocation(location, protocol) {
114
140
  }
115
141
  throw new Error(`invalid protocol ${protocol}`);
116
142
  }
143
+ // ordered most-specific-first so e.g. `.vcf.gz` matches before `.vcf`.
144
+ //
145
+ // `.gtf.gz` maps to GtfAdapter rather than GtfTabixAdapter on purpose: this
146
+ // guesser only invents a throwaway track for `text-index --file`, which streams
147
+ // the file start to finish and never opens the .tbi, and GtfAdapter's
148
+ // gtfLocation takes a gzipped path (the reader gunzips on the suffix). A track
149
+ // in a config.json is the other case — `jbrowse add-track` gives a bgzipped GTF
150
+ // a GtfTabixAdapter, which is indexable too, from its own gtfGzLocation.
151
+ const adapterGuesses = [
152
+ { regex: /\.vcf\.b?gz$/i, type: 'VcfTabixAdapter' },
153
+ { regex: /\.gff3?\.b?gz$/i, type: 'Gff3TabixAdapter' },
154
+ { regex: /\.gtf(\.b?gz)?$/i, type: 'GtfAdapter' },
155
+ { regex: /\.vcf$/i, type: 'VcfAdapter' },
156
+ { regex: /\.gff3?$/i, type: 'Gff3Adapter' },
157
+ ];
117
158
  export function guessAdapterFromFileName(filePath) {
118
- const protocol = isURL(filePath) ? 'uri' : 'localPath';
119
- const name = path.basename(filePath);
120
- if (/\.vcf\.b?gz$/i.test(filePath)) {
121
- return {
122
- trackId: name,
123
- name: name,
124
- assemblyNames: [],
125
- adapter: {
126
- type: 'VcfTabixAdapter',
127
- vcfGzLocation: makeLocation(filePath, protocol),
128
- },
129
- };
130
- }
131
- else if (/\.gff3?\.b?gz$/i.test(filePath)) {
159
+ const guess = adapterGuesses.find(g => g.regex.test(filePath));
160
+ if (guess) {
161
+ const name = path.basename(filePath);
162
+ const protocol = isURL(filePath) ? 'uri' : 'localPath';
132
163
  return {
133
164
  trackId: name,
134
165
  name,
135
166
  assemblyNames: [],
136
167
  adapter: {
137
- type: 'Gff3TabixAdapter',
138
- gffGzLocation: makeLocation(filePath, protocol),
139
- },
140
- };
141
- }
142
- else if (/\.gtf?$/i.test(filePath)) {
143
- return {
144
- trackId: name,
145
- name,
146
- assemblyNames: [],
147
- adapter: {
148
- type: 'GtfAdapter',
149
- gtfLocation: makeLocation(filePath, protocol),
150
- },
151
- };
152
- }
153
- else if (/\.vcf$/i.test(filePath)) {
154
- return {
155
- trackId: name,
156
- name,
157
- assemblyNames: [],
158
- adapter: {
159
- type: 'VcfAdapter',
160
- vcfLocation: makeLocation(filePath, protocol),
161
- },
162
- };
163
- }
164
- else if (/\.gff3?$/i.test(filePath)) {
165
- return {
166
- trackId: name,
167
- name,
168
- assemblyNames: [],
169
- adapter: {
170
- type: 'Gff3Adapter',
171
- gffLocation: makeLocation(filePath, protocol),
168
+ type: guess.type,
169
+ [indexableAdapters[guess.type].locationKey]: makeLocation(filePath, protocol),
172
170
  },
173
171
  };
174
172
  }
@@ -176,23 +174,3 @@ export function guessAdapterFromFileName(filePath) {
176
174
  throw new Error(`Unsupported file type ${filePath}`);
177
175
  }
178
176
  }
179
- export function sanitizeForFilename(name) {
180
- return name.replace(/[\\/:*?"<>|]/g, '_');
181
- }
182
- export function generateMeta({ configs, attributesToIndex, outDir, name, featureTypesToExclude, assemblyNames, }) {
183
- const safeName = sanitizeForFilename(name);
184
- fs.writeFileSync(path.join(outDir, 'trix', `${safeName}_meta.json`), JSON.stringify({
185
- dateCreated: new Date().toISOString(),
186
- tracks: configs.map(config => {
187
- const { trackId, textSearching, adapter } = config;
188
- return {
189
- trackId,
190
- attributesIndexed: textSearching?.indexingAttributes || attributesToIndex,
191
- excludedTypes: textSearching?.indexingFeatureTypesToExclude ||
192
- featureTypesToExclude,
193
- adapterConf: adapter,
194
- };
195
- }),
196
- assemblyNames,
197
- }, null, 2));
198
- }
@@ -1,11 +1,20 @@
1
- export declare function indexGff3({ config, attributesToIndex, inLocation, outDir, featureTypesToExclude, onStart, onUpdate, }: {
2
- config: {
3
- trackId: string;
4
- };
5
- attributesToIndex: string[];
6
- inLocation: string;
7
- outDir: string;
8
- featureTypesToExclude: string[];
9
- onStart: (totalBytes: number) => void;
10
- onUpdate: (progressBytes: number) => void;
11
- }): AsyncGenerator<string, void, unknown>;
1
+ import type { Gff3IndexerOptions } from '../util.ts';
2
+ /**
3
+ * Stream one GFF3 into trix records, one line per indexed feature.
4
+ *
5
+ * Two type gates, and they answer different questions. `featureTypesToExclude`
6
+ * names types this file is known to carry and nobody would search for — it is
7
+ * the right shape when you own the vocabulary, which is why the default is
8
+ * `exon,CDS`. `featureTypesToInclude` names the types worth indexing and drops
9
+ * everything else, which is the only shape that stays correct against a
10
+ * vocabulary someone else controls: an NCBI RefSeq GFF3 draws from 115 feature
11
+ * types, 80 of which are leaf regulatory/repeat/alignment records carrying no
12
+ * name (a `match` labels itself with a bare UUID, a `cDNA_match` with an MD5,
13
+ * every `biological_region` with the literal string "biological region"), and a
14
+ * deny list written against the types seen so far leaks the next one NCBI adds.
15
+ * The allow list for that file is ~33 names — gene, pseudogene, and the
16
+ * transcript-level types — and does not grow.
17
+ *
18
+ * Both apply when both are given: include admits, exclude then narrows.
19
+ */
20
+ export declare function indexGff3({ config, attributesToIndex, inLocation, outDir, featureTypesToExclude, featureTypesToInclude, onStart, onUpdate, checkAbort, }: Gff3IndexerOptions): AsyncGenerator<string, void, unknown>;
@@ -1,6 +1,38 @@
1
- import { decodeURIComponentNoThrow } from "../util.js";
2
- import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from "./common.js";
3
- export async function* indexGff3({ config, attributesToIndex, inLocation, outDir, featureTypesToExclude, onStart, onUpdate, }) {
1
+ import { decodeURIComponentNoThrow, trixLine } from '../util.js';
2
+ import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from './common.js';
3
+ // GENCODE writes no `Name`: a gene row carries `gene_name`, a transcript row
4
+ // `transcript_name`, and every row repeats its parents' names too. So `Name`
5
+ // falls back to the name whose id is this row's own ID, which leaves a UTR or
6
+ // codon row unnamed rather than answering a search for its transcript.
7
+ function ownName(attrs) {
8
+ const id = attrs.ID;
9
+ return id === undefined
10
+ ? undefined
11
+ : id === attrs.transcript_id
12
+ ? attrs.transcript_name
13
+ : id === attrs.gene_id
14
+ ? attrs.gene_name
15
+ : undefined;
16
+ }
17
+ /**
18
+ * Stream one GFF3 into trix records, one line per indexed feature.
19
+ *
20
+ * Two type gates, and they answer different questions. `featureTypesToExclude`
21
+ * names types this file is known to carry and nobody would search for — it is
22
+ * the right shape when you own the vocabulary, which is why the default is
23
+ * `exon,CDS`. `featureTypesToInclude` names the types worth indexing and drops
24
+ * everything else, which is the only shape that stays correct against a
25
+ * vocabulary someone else controls: an NCBI RefSeq GFF3 draws from 115 feature
26
+ * types, 80 of which are leaf regulatory/repeat/alignment records carrying no
27
+ * name (a `match` labels itself with a bare UUID, a `cDNA_match` with an MD5,
28
+ * every `biological_region` with the literal string "biological region"), and a
29
+ * deny list written against the types seen so far leaks the next one NCBI adds.
30
+ * The allow list for that file is ~33 names — gene, pseudogene, and the
31
+ * transcript-level types — and does not grow.
32
+ *
33
+ * Both apply when both are given: include admits, exclude then narrows.
34
+ */
35
+ export async function* indexGff3({ config, attributesToIndex, inLocation, outDir, featureTypesToExclude, featureTypesToInclude, onStart, onUpdate, checkAbort, }) {
4
36
  const { trackId } = config;
5
37
  const stream = await getLocalOrRemoteStream({
6
38
  file: inLocation,
@@ -10,29 +42,41 @@ export async function* indexGff3({ config, attributesToIndex, inLocation, outDir
10
42
  });
11
43
  const rl = createReadlineInterface(stream, inLocation);
12
44
  const excludeSet = new Set(featureTypesToExclude);
13
- const encodedTrackId = encodeURIComponent(trackId);
45
+ // Absent rather than empty, so an empty list means "no allow list" and not
46
+ // "index nothing" — a caller that reads the list out of a config gets the
47
+ // former when the slot is unset, and silently indexing zero features would
48
+ // look exactly like a successful run.
49
+ const includeSet = featureTypesToInclude?.length
50
+ ? new Set(featureTypesToInclude)
51
+ : undefined;
14
52
  for await (const line of rl) {
15
- if (!line.trim()) {
16
- continue;
17
- }
18
- else if (line.startsWith('#')) {
53
+ checkAbort?.();
54
+ if (!line.trim() || line.startsWith('#')) {
19
55
  continue;
20
56
  }
21
57
  else if (line.startsWith('>')) {
22
58
  break;
23
59
  }
24
60
  const [seq_id, , type, start, end, , , , col9] = line.split('\t');
25
- if (!excludeSet.has(type)) {
61
+ // a valid GFF3 feature row has 9 tab-delimited columns; skip malformed or
62
+ // truncated lines that would otherwise make parseAttributes throw on an
63
+ // undefined column 9 (mirrors the guard in the vcf indexer)
64
+ if (seq_id === undefined ||
65
+ type === undefined ||
66
+ start === undefined ||
67
+ end === undefined ||
68
+ col9 === undefined) {
69
+ continue;
70
+ }
71
+ if (!excludeSet.has(type) &&
72
+ (includeSet === undefined || includeSet.has(type))) {
26
73
  const col9attrs = parseAttributes(col9, decodeURIComponentNoThrow);
27
74
  const attrs = attributesToIndex
28
- .map(attr => col9attrs[attr])
75
+ .map(attr => col9attrs[attr] ??
76
+ (attr === 'Name' ? ownName(col9attrs) : undefined))
29
77
  .filter((f) => !!f);
30
78
  if (attrs.length > 0) {
31
- const locStr = `${seq_id}:${start}..${end}`;
32
- const encodedAttrs = attrs.map(a => `"${encodeURIComponent(a)}"`);
33
- const record = `["${encodeURIComponent(locStr)}"|"${encodedTrackId}"|${encodedAttrs.join('|')}]`;
34
- const uniqueAttrs = [...new Set(attrs)];
35
- yield `${record} ${uniqueAttrs.join(' ')}\n`;
79
+ yield trixLine(`${seq_id}:${start}..${end}`, trackId, attrs);
36
80
  }
37
81
  }
38
82
  }
@@ -0,0 +1,25 @@
1
+ import type { IndexerOptions } from '../util.ts';
2
+ /**
3
+ * Parse the GTF 9th column (`gene_id "X"; transcript_id "Y";`), mirroring
4
+ * plugins/gtf so the indexed text matches what the track displays: entries are
5
+ * `key value` separated by a space (not `key=value`), values may be quoted, a
6
+ * repeated key expresses multiple values, and a quoted value may contain the
7
+ * ';' the entries are split on (`note "a; b"`), so its pieces are rejoined
8
+ * before the value is read.
9
+ */
10
+ export declare function parseGtfAttributes(attrString: string): Record<string, string[]>;
11
+ /**
12
+ * GTF, unlike GFF3, has no gene or transcript rows: a gene exists only as the
13
+ * exon/CDS/UTR rows repeating its `gene_id`. Indexing per row would point every
14
+ * hit at a single exon, so rows are grouped by `gene_id` and `transcript_id`
15
+ * and each group is emitted once spanning all of its rows.
16
+ *
17
+ * Grouping is what keeps one entry per gene rather than one per exon, so
18
+ * `featureTypesToExclude` does not apply here — dropping rows would only
19
+ * truncate the spans.
20
+ *
21
+ * The group map is held until EOF because GTF rows for one locus are not
22
+ * guaranteed contiguous. It is keyed by gene/transcript id, so it stays far
23
+ * smaller than the whole-file parse plugins/gtf already does to display a GTF.
24
+ */
25
+ export declare function indexGtf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }: IndexerOptions): AsyncGenerator<string, void, unknown>;
@@ -0,0 +1,146 @@
1
+ import { trixLine } from '../util.js';
2
+ import { createReadlineInterface, getLocalOrRemoteStream } from './common.js';
3
+ // GFF3-style attribute names have no GTF equivalent, so the shared defaults
4
+ // (`Name`, `ID`, `symbol`) would index nothing. Each requested name also pulls
5
+ // in its GTF spellings, so `jbrowse text-index` works on a GTF out of the box
6
+ // while an explicit `--attributes gene_biotype` still works verbatim.
7
+ const gtfAttributeAliases = {
8
+ Name: ['gene_name', 'transcript_name'],
9
+ ID: ['gene_id', 'transcript_id'],
10
+ symbol: ['gene_name'],
11
+ };
12
+ function expandAttributeNames(attributesToIndex) {
13
+ return [
14
+ ...new Set(attributesToIndex.flatMap(a => [a, ...(gtfAttributeAliases[a] ?? [])])),
15
+ ];
16
+ }
17
+ /**
18
+ * True when an entry opens a double quote it never closes, meaning the ';' it
19
+ * was split on fell inside the value instead of ending the entry.
20
+ */
21
+ function hasUnclosedQuote(entry) {
22
+ const first = entry.indexOf('"');
23
+ return first !== -1 && !entry.includes('"', first + 1);
24
+ }
25
+ /**
26
+ * Parse the GTF 9th column (`gene_id "X"; transcript_id "Y";`), mirroring
27
+ * plugins/gtf so the indexed text matches what the track displays: entries are
28
+ * `key value` separated by a space (not `key=value`), values may be quoted, a
29
+ * repeated key expresses multiple values, and a quoted value may contain the
30
+ * ';' the entries are split on (`note "a; b"`), so its pieces are rejoined
31
+ * before the value is read.
32
+ */
33
+ export function parseGtfAttributes(attrString) {
34
+ const attrs = {};
35
+ const entries = attrString.split(';');
36
+ for (let i = 0; i < entries.length; i++) {
37
+ let entry = entries[i];
38
+ while (hasUnclosedQuote(entry) && i + 1 < entries.length) {
39
+ entry += `;${entries[++i]}`;
40
+ }
41
+ const trimmed = entry.trim();
42
+ const sp = trimmed.indexOf(' ');
43
+ if (sp !== -1) {
44
+ const key = trimmed.slice(0, sp);
45
+ const value = trimmed
46
+ .slice(sp + 1)
47
+ .trim()
48
+ .replaceAll(/^"|"$/g, '')
49
+ // a comma separates record ids in the trix .ix format, so it can never
50
+ // survive into an indexed word
51
+ .replaceAll(',', ' ');
52
+ if (value) {
53
+ ;
54
+ (attrs[key] ??= []).push(value);
55
+ }
56
+ }
57
+ }
58
+ return attrs;
59
+ }
60
+ // composite map keys join refName + id; a separator that cannot occur in
61
+ // either keeps two loci from colliding
62
+ const SEP = '\u0000';
63
+ /**
64
+ * GTF, unlike GFF3, has no gene or transcript rows: a gene exists only as the
65
+ * exon/CDS/UTR rows repeating its `gene_id`. Indexing per row would point every
66
+ * hit at a single exon, so rows are grouped by `gene_id` and `transcript_id`
67
+ * and each group is emitted once spanning all of its rows.
68
+ *
69
+ * Grouping is what keeps one entry per gene rather than one per exon, so
70
+ * `featureTypesToExclude` does not apply here — dropping rows would only
71
+ * truncate the spans.
72
+ *
73
+ * The group map is held until EOF because GTF rows for one locus are not
74
+ * guaranteed contiguous. It is keyed by gene/transcript id, so it stays far
75
+ * smaller than the whole-file parse plugins/gtf already does to display a GTF.
76
+ */
77
+ export async function* indexGtf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }) {
78
+ const { trackId } = config;
79
+ const stream = await getLocalOrRemoteStream({
80
+ file: inLocation,
81
+ out: outDir,
82
+ onStart,
83
+ onUpdate,
84
+ });
85
+ const rl = createReadlineInterface(stream, inLocation);
86
+ const attributeNames = expandAttributeNames(attributesToIndex);
87
+ const groups = new Map();
88
+ function accumulate(key, refName, start, end, values) {
89
+ if (values.length > 0) {
90
+ const group = groups.get(key);
91
+ if (group) {
92
+ group.start = Math.min(group.start, start);
93
+ group.end = Math.max(group.end, end);
94
+ for (const value of values) {
95
+ group.attrs.add(value);
96
+ }
97
+ }
98
+ else {
99
+ groups.set(key, { refName, start, end, attrs: new Set(values) });
100
+ }
101
+ }
102
+ }
103
+ for await (const line of rl) {
104
+ checkAbort?.();
105
+ if (line.trim() && !line.startsWith('#')) {
106
+ const [seq_id, , , start, end, , , , col9] = line.split('\t');
107
+ if (seq_id !== undefined &&
108
+ start !== undefined &&
109
+ end !== undefined &&
110
+ col9 !== undefined) {
111
+ const parsed = parseGtfAttributes(col9);
112
+ const geneId = parsed.gene_id?.[0];
113
+ const transcriptId = parsed.transcript_id?.[0];
114
+ const s = Number(start);
115
+ const e = Number(end);
116
+ // `gene_*` names describe the gene, everything else the transcript, so
117
+ // a gene entry is labeled by its gene_name rather than by whichever
118
+ // transcript happened to come first
119
+ const geneValues = [];
120
+ const otherValues = [];
121
+ for (const name of attributeNames) {
122
+ for (const value of parsed[name] ?? []) {
123
+ ;
124
+ (name.startsWith('gene') ? geneValues : otherValues).push(value);
125
+ }
126
+ }
127
+ if (transcriptId === undefined) {
128
+ const values = [...geneValues, ...otherValues];
129
+ // no id to group on: merge rows that carry identical values, which
130
+ // still collapses the repeated rows of one feature into one entry
131
+ const key = geneId ?? values.join(SEP);
132
+ accumulate(`gene${SEP}${seq_id}${SEP}${key}`, seq_id, s, e, values);
133
+ }
134
+ else {
135
+ accumulate(`tx${SEP}${seq_id}${SEP}${transcriptId}`, seq_id, s, e, otherValues);
136
+ if (geneId !== undefined) {
137
+ accumulate(`gene${SEP}${seq_id}${SEP}${geneId}`, seq_id, s, e, geneValues);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ for (const { refName, start, end, attrs } of groups.values()) {
144
+ yield trixLine(`${refName}:${start}..${end}`, trackId, [...attrs]);
145
+ }
146
+ }
@@ -1,10 +1,2 @@
1
- export declare function indexVcf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, }: {
2
- config: {
3
- trackId: string;
4
- };
5
- attributesToIndex: string[];
6
- inLocation: string;
7
- outDir: string;
8
- onStart: (totalBytes: number) => void;
9
- onUpdate: (progressBytes: number) => void;
10
- }): AsyncGenerator<string, void, unknown>;
1
+ import type { IndexerOptions } from '../util.ts';
2
+ export declare function indexVcf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }: IndexerOptions): AsyncGenerator<string, void, unknown>;
@@ -1,6 +1,6 @@
1
- import { decodeURIComponentNoThrow } from "../util.js";
2
- import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from "./common.js";
3
- export async function* indexVcf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, }) {
1
+ import { decodeURIComponentNoThrow, trixLine } from '../util.js';
2
+ import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from './common.js';
3
+ export async function* indexVcf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }) {
4
4
  const { trackId } = config;
5
5
  const stream = await getLocalOrRemoteStream({
6
6
  file: inLocation,
@@ -9,27 +9,42 @@ export async function* indexVcf({ config, attributesToIndex, inLocation, outDir,
9
9
  onUpdate,
10
10
  });
11
11
  const rl = createReadlineInterface(stream, inLocation);
12
- const encodedTrackId = encodeURIComponent(trackId);
13
12
  for await (const line of rl) {
14
- if (line.startsWith('#')) {
13
+ checkAbort?.();
14
+ if (!line.trim() || line.startsWith('#')) {
15
15
  continue;
16
16
  }
17
- const [ref, pos, id, _ref, _alt, _qual, _filter, info] = line.split('\t');
18
- if (id === '.') {
17
+ const [ref, pos, id, , , , , info] = line.split('\t');
18
+ // a valid VCF data row has at least 8 tab-delimited columns; skip blank or
19
+ // malformed/short lines that would otherwise emit garbage records or throw
20
+ if (ref === undefined ||
21
+ pos === undefined ||
22
+ id === undefined ||
23
+ info === undefined) {
19
24
  continue;
20
25
  }
21
26
  const fields = parseAttributes(info, decodeURIComponentNoThrow);
22
- const end = fields.END;
23
- const locStr = `${ref}:${pos}..${end || +pos + 1}`;
24
- const encodedLocStr = encodeURIComponent(locStr);
27
+ // an empty `END=` parses to 0 via Number(''), which is finite; require a
28
+ // non-empty value before trusting it, else fall back to pos+1
29
+ const endNum = Number(fields.END);
30
+ const end = fields.END && Number.isFinite(endNum) ? endNum : Number(pos) + 1;
31
+ const locStr = `${ref}:${pos}..${end}`;
25
32
  const infoAttrs = attributesToIndex
26
33
  .map(attr => fields[attr])
27
34
  .filter((f) => !!f);
28
- const encodedInfoAttrs = infoAttrs.map(a => `"${encodeURIComponent(a)}"`);
29
- for (const variantId of id.split(',')) {
30
- const encodedId = encodeURIComponent(variantId);
31
- const record = `["${encodedLocStr}"|"${encodedTrackId}"|"${encodedId}"${encodedInfoAttrs.length > 0 ? `|${encodedInfoAttrs.join('|')}` : ''}]`;
32
- yield `${record} ${variantId}\n`;
35
+ // VCF separates multiple IDs with ';' (matching @gmod/vcf), not ','. '.' is
36
+ // the no-id sentinel; such a row is still worth indexing by its INFO
37
+ // attributes, it just has no id to search by
38
+ const variantIds = id === '.' ? [] : id.split(';').filter(Boolean);
39
+ // one record per id so each is separately searchable, plus an id-less
40
+ // record when the row carries only attributes
41
+ const attrGroups = variantIds.length
42
+ ? variantIds.map(variantId => [variantId, ...infoAttrs])
43
+ : [infoAttrs];
44
+ for (const attrs of attrGroups) {
45
+ if (attrs.length > 0) {
46
+ yield trixLine(locStr, trackId, attrs);
47
+ }
33
48
  }
34
49
  }
35
50
  }
package/esm/util.d.ts CHANGED
@@ -28,18 +28,70 @@ export interface VcfAdapter {
28
28
  vcfLocation: Loc;
29
29
  }
30
30
  export interface Track {
31
+ type?: string;
31
32
  adapter?: {
32
33
  type: string;
33
34
  [key: string]: unknown;
34
35
  };
35
36
  textSearching?: {
36
37
  indexingFeatureTypesToExclude?: string[];
38
+ indexingFeatureTypesToInclude?: string[];
37
39
  indexingAttributes?: string[];
38
40
  [key: string]: unknown;
39
41
  };
42
+ metadata?: {
43
+ skipTextIndex?: boolean;
44
+ [key: string]: unknown;
45
+ };
40
46
  name: string;
41
47
  assemblyNames: string[];
42
48
  trackId: string;
49
+ category?: string[];
50
+ description?: string;
51
+ }
52
+ export declare const defaultAttributesToIndex: string[];
53
+ export declare const defaultFeatureTypesToExclude: string[];
54
+ /** What an index reads out of a track's features. */
55
+ export interface IndexingPolicy {
56
+ attributes: string[];
57
+ /** feature types to skip (GFF3 only) */
58
+ exclude: string[];
59
+ /**
60
+ * the only feature types to index, narrowed by `exclude` (GFF3 only). Absent
61
+ * or empty means no allow list, never "index nothing". See indexGff3
62
+ */
63
+ include?: string[];
64
+ }
65
+ export declare const defaultIndexingPolicy: IndexingPolicy;
66
+ /** The policy `track` is indexed under: its own settings, else the run's. */
67
+ export declare function trackIndexingPolicy({ textSearching }: Track, run: IndexingPolicy): IndexingPolicy;
68
+ export type IndexableFormat = 'gff3' | 'gtf' | 'vcf';
69
+ export declare const indexableAdapters: Record<string, {
70
+ locationKey: string;
71
+ format: IndexableFormat;
72
+ }>;
73
+ export declare function isSupportedIndexingAdapter(type?: string): boolean;
74
+ export interface IndexerOptions {
75
+ config: {
76
+ trackId: string;
77
+ };
78
+ attributesToIndex: string[];
79
+ inLocation: string;
80
+ outDir: string;
81
+ onStart: (totalBytes: number) => void;
82
+ onUpdate: (progressBytes: number) => void;
83
+ checkAbort?: () => void;
84
+ }
85
+ export interface Gff3IndexerOptions extends IndexerOptions {
86
+ featureTypesToExclude: string[];
87
+ featureTypesToInclude?: string[];
43
88
  }
89
+ /**
90
+ * One line of ixIxx input: the record a search hit hands back —
91
+ * `["loc"|"trackId"|"attr"|...]`, fields URI-encoded so neither a comma nor a
92
+ * space can split it — then the words the hit is found by.
93
+ * TrixTextSearchAdapter reads the record back.
94
+ */
95
+ export declare function trixLine(loc: string, trackId: string, attrs: string[]): string;
44
96
  export declare function decodeURIComponentNoThrow(uri: string): string;
45
97
  export {};
package/esm/util.js CHANGED
@@ -1,8 +1,47 @@
1
+ export const defaultAttributesToIndex = ['Name', 'ID', 'symbol'];
2
+ export const defaultFeatureTypesToExclude = ['exon', 'CDS'];
3
+ export const defaultIndexingPolicy = {
4
+ attributes: defaultAttributesToIndex,
5
+ exclude: defaultFeatureTypesToExclude,
6
+ };
7
+ /** The policy `track` is indexed under: its own settings, else the run's. */
8
+ export function trackIndexingPolicy({ textSearching }, run) {
9
+ return {
10
+ attributes: textSearching?.indexingAttributes ?? run.attributes,
11
+ exclude: textSearching?.indexingFeatureTypesToExclude ?? run.exclude,
12
+ include: textSearching?.indexingFeatureTypesToInclude ?? run.include,
13
+ };
14
+ }
15
+ // single source of truth for what `jbrowse text-index` can index: which adapter
16
+ // types, where each keeps its file location, and which line parser to use.
17
+ // Everything else (the adapter dispatch, the CLI's supported() gate, the
18
+ // filename guesser) derives from this table.
19
+ export const indexableAdapters = {
20
+ Gff3Adapter: { locationKey: 'gffLocation', format: 'gff3' },
21
+ Gff3TabixAdapter: { locationKey: 'gffGzLocation', format: 'gff3' },
22
+ GtfAdapter: { locationKey: 'gtfLocation', format: 'gtf' },
23
+ GtfTabixAdapter: { locationKey: 'gtfGzLocation', format: 'gtf' },
24
+ VcfAdapter: { locationKey: 'vcfLocation', format: 'vcf' },
25
+ VcfTabixAdapter: { locationKey: 'vcfGzLocation', format: 'vcf' },
26
+ };
27
+ export function isSupportedIndexingAdapter(type = '') {
28
+ return type in indexableAdapters;
29
+ }
30
+ /**
31
+ * One line of ixIxx input: the record a search hit hands back —
32
+ * `["loc"|"trackId"|"attr"|...]`, fields URI-encoded so neither a comma nor a
33
+ * space can split it — then the words the hit is found by.
34
+ * TrixTextSearchAdapter reads the record back.
35
+ */
36
+ export function trixLine(loc, trackId, attrs) {
37
+ const fields = [loc, trackId, ...attrs].map(f => `"${encodeURIComponent(f)}"`);
38
+ return `[${fields.join('|')}] ${[...new Set(attrs)].join(' ')}\n`;
39
+ }
1
40
  export function decodeURIComponentNoThrow(uri) {
2
41
  try {
3
42
  return decodeURIComponent(uri);
4
43
  }
5
- catch (e) {
44
+ catch {
6
45
  return uri;
7
46
  }
8
47
  }
@@ -0,0 +1,19 @@
1
+ import type { TrackIndexProgress } from './indexFiles.ts';
2
+ import type { IndexingPolicy, Track } from './util.ts';
3
+ /**
4
+ * Index `tracks` into `<outDir>/trix/<name>.ix`, `.ixx` and `_meta.json`.
5
+ *
6
+ * `onSort` fires once every file has been read, when the sort that writes the
7
+ * index begins — minutes on a large index, with no byte count to report.
8
+ */
9
+ export declare function writeTrixIndex({ tracks, outDir, name, policy, assemblyNames, prefixSize, makeProgress, checkAbort, onSort, }: {
10
+ tracks: Track[];
11
+ outDir: string;
12
+ name: string;
13
+ policy: IndexingPolicy;
14
+ assemblyNames: string[];
15
+ prefixSize?: number;
16
+ makeProgress?: (trackId: string) => TrackIndexProgress;
17
+ checkAbort?: () => void;
18
+ onSort?: () => void;
19
+ }): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Readable } from 'node:stream';
4
+ import { ixIxxStream } from 'ixixx';
5
+ import { indexFiles } from './indexFiles.js';
6
+ import { TRIX_DIR, trixFilePaths } from './trixPaths.js';
7
+ import { trackIndexingPolicy } from './util.js';
8
+ /**
9
+ * Index `tracks` into `<outDir>/trix/<name>.ix`, `.ixx` and `_meta.json`.
10
+ *
11
+ * `onSort` fires once every file has been read, when the sort that writes the
12
+ * index begins — minutes on a large index, with no byte count to report.
13
+ */
14
+ export async function writeTrixIndex({ tracks, outDir, name, policy, assemblyNames, prefixSize, makeProgress, checkAbort, onSort, }) {
15
+ fs.mkdirSync(path.join(outDir, TRIX_DIR), { recursive: true });
16
+ const records = Readable.from(indexFiles({ tracks, policy, outDir, makeProgress, checkAbort }));
17
+ if (onSort) {
18
+ records.on('end', onSort);
19
+ }
20
+ const paths = trixFilePaths(outDir, name);
21
+ await ixIxxStream(records, paths.ix, paths.ixx, prefixSize);
22
+ checkAbort?.();
23
+ fs.writeFileSync(paths.meta, JSON.stringify({
24
+ dateCreated: new Date().toISOString(),
25
+ tracks: tracks.map(track => {
26
+ const { attributes, exclude, include } = trackIndexingPolicy(track, policy);
27
+ return {
28
+ trackId: track.trackId,
29
+ attributesIndexed: attributes,
30
+ excludedTypes: exclude,
31
+ includedTypes: include,
32
+ adapterConf: track.adapter,
33
+ };
34
+ }),
35
+ assemblyNames,
36
+ }, null, 2));
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/text-indexing-core",
3
- "version": "4.3.0",
3
+ "version": "5.0.0-beta.10",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 core text indexing routines for parsing GFF3 and VCF files",
6
6
  "keywords": [
@@ -22,21 +22,20 @@
22
22
  "main": "esm/index.js",
23
23
  "types": "esm/index.d.ts",
24
24
  "exports": {
25
- ".": {
26
- "types": "./esm/index.d.ts",
27
- "import": "./esm/index.js"
28
- }
25
+ ".": "./esm/index.js"
29
26
  },
30
27
  "files": [
31
28
  "esm"
32
29
  ],
33
- "dependencies": {},
30
+ "dependencies": {
31
+ "ixixx": "^3.0.13"
32
+ },
34
33
  "publishConfig": {
35
34
  "access": "public"
36
35
  },
37
36
  "sideEffects": false,
38
37
  "scripts": {
39
- "build:esm": "tsc -p tsconfig.build.esm.json",
38
+ "build:esm": "node ../../node_modules/typescript7/bin/tsc --build tsconfig.build.esm.json",
40
39
  "build": "pnpm build:esm",
41
40
  "test": "cd ../..; jest --passWithNoTests packages/text-indexing-core",
42
41
  "clean": "rimraf esm *.tsbuildinfo",